ConversationChatContainer.vue 57 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024
  1. <template>
  2. <div class="conversation-chat-container">
  3. <div class="conversation-chat-main" ref="chat_scrren_container">
  4. <div class="conversation-chat-main-left"></div>
  5. <div class="conversation-chat-main-content" :style="style_conversation_chat_content">
  6. <DialogHeader
  7. v-if="!show_welcome && !is_init_model"
  8. class="conversation-chat-header"
  9. :agent_name="current_agent_name"
  10. :style="style_header"
  11. />
  12. <div class="conversation-chat-content" :style="style_body">
  13. <div class="conversation-chat-main-container" :style="style_conversation_chat_main_container">
  14. <template v-if="show_welcome">
  15. <!-- 有会话ID 获取历史记录,显示加载中状态 -->
  16. <!-- <div v-if="session_id" class="conversation-chat-area history-list-loading"> -->
  17. <!-- <div class="scroll-left"></div> -->
  18. <!-- <div class="history-list-loading-container"><a-spin /></div> -->
  19. <!-- <div class="scroll-right"></div> -->
  20. <!-- </div> -->
  21. <!-- 无会话ID,直接对话,此时可现实欢迎消息:欢迎使用报表合并、欢迎使用文档智能、欢迎使用小数绘图 -->
  22. <!-- <DialogWelcome v-if="!session_id" class="conversation-chat-area" /> -->
  23. <DialogWelcome class="conversation-chat-area" />
  24. </template>
  25. <DialogList
  26. v-if="!show_welcome"
  27. ref="list_scroller"
  28. class="conversation-chat-area"
  29. :height="list_height"
  30. :width="chat_width / 2"
  31. :agent_id="agent_id"
  32. :list="chat_display_list"
  33. :suggestion="chat_suggestion"
  34. :answer_action_list="answer_action_list"
  35. :is_show_right_page="show_right"
  36. :loading_mind_map="loading_mind_map"
  37. @share="handleChatShare"
  38. @edit="handleEditChat"
  39. @retry="handleRetryChat"
  40. @toSearchPage="toSearchPage"
  41. @toShowMindMap="toShowMindMap"
  42. @send="handleSendChat"
  43. @submit="handleSuggestSubmit"
  44. :style="style_conversation_chat_list"
  45. >
  46. <!-- <template v-slot:left>
  47. <div class="scroll-left"></div>
  48. </template>
  49. <template v-slot:right>
  50. <div class="scroll-right"></div>
  51. </template> -->
  52. </DialogList>
  53. </div>
  54. <DialogInput
  55. v-show="!is_init_model"
  56. ref="dialog_input"
  57. class="conversation-chat-input"
  58. :style="style_conversation_chat_list"
  59. :ask_input_text="ask_input_text"
  60. :disabled="input_disabled"
  61. :enable_text_input="enable_text_input"
  62. :enable_file_upload="enable_file_upload"
  63. :file_accept="file_accept"
  64. :agent_id="agent_id"
  65. @sizeChange="handleInputSizeChange"
  66. @actionConfigChange="handleActionConfigChange"
  67. @submit="handleInputSubmit"
  68. >
  69. <template v-if="show_input_others_opts" v-slot:top>
  70. <DialogInputTop
  71. :user_input_form="conversation_params.user_input_form"
  72. @initModel="initModel"
  73. ref="dialog_input_top"
  74. />
  75. </template>
  76. <template v-slot:top>
  77. <DialogStopSse v-if="show_stop_sse" @stopResponse="stopResponse"></DialogStopSse>
  78. </template>
  79. </DialogInput>
  80. <DialogCommonUsed
  81. v-if="show_welcome"
  82. :style="style_conversation_chat_list"
  83. @updateAskTxt="updatAskTxt"
  84. @quickSend="quickSend"
  85. ></DialogCommonUsed>
  86. </div>
  87. <DialogFooter v-show="!is_init_model" class="conversation-chat-footer" :style="style_footer" />
  88. </div>
  89. <div class="conversation-chat-main-right" :style="style_conversation_chat_right">
  90. <div class="conversation-chat-expand" :style="style_conversation_chat_expand" v-if="is_show_right_page">
  91. <div class="conversation-chat-mind-map" :style="style_conversation_chat_mind_map" ref="fullScreen_mindMap">
  92. <!-- 全屏时显示的退出按钮 -->
  93. <button v-if="isFullScreen" class="exit-button" @click="exitFullScreen">X</button>
  94. <MindMap
  95. v-if="show_mind_map"
  96. ref="mind_map"
  97. :content="mindMap_content"
  98. :box_style="box_style"
  99. :svg_style="svg_style"
  100. @extend="extendMindMap"
  101. ></MindMap>
  102. <div class="mind_map_error" :style="style_map_error" v-if="mindMap_err.length">{{ mindMap_err }}</div>
  103. <div class="conversation-chat-mind-map-tools">
  104. <ConversationChatMindMapTools
  105. :isFullScreen="isFullScreen"
  106. @toZoomIn="zoomIn"
  107. @toZoomOut="zoomOut"
  108. @toZoomReset="zoomReset"
  109. @toDownload="downloadMap"
  110. @toFullScreen="toFullScreen"
  111. @toExitFullScreen="exitFullScreen"
  112. ></ConversationChatMindMapTools>
  113. </div>
  114. </div>
  115. <div class="conversation-chat-mind-map-spin" :style="style_map_error" v-if="loading_mind_map">
  116. <a-spin tip="加载中..."></a-spin>
  117. </div>
  118. <div class="conversation-chat-web-page" :style="style_conversation_chat_web_page" v-if="show_web_page">
  119. <ConversationChatWebPage :search_list="search_list" @toClose="closeWebPage"></ConversationChatWebPage>
  120. </div>
  121. </div>
  122. <div class="conversation-chat-expand" v-if="show_word_viewer">
  123. <ConversationChatWordViewer :edit_txt="edit_txt_value" @close="closeChatWord"></ConversationChatWordViewer>
  124. </div>
  125. </div>
  126. </div>
  127. <ChatAsideExpandBtn v-if="show_right_page_btn" @toggleRight="toggleRight"></ChatAsideExpandBtn>
  128. </div>
  129. </template>
  130. <script setup>
  131. import { ref, onMounted, onUnmounted, watch, computed, nextTick } from 'vue';
  132. import { useI18n } from 'vue-i18n';
  133. import { marked } from 'marked';
  134. // import { Message } from '@arco-design/web-vue';
  135. import { Message } from '../../3rd-libs/arco-vue-libs';
  136. import { jsonStringify } from '../../utils/utils';
  137. import DialogHeader from '../../modules/conversation-chat/DialogHeader.vue';
  138. import DialogWelcome from '../../modules/conversation-chat/DialogWelcome.vue';
  139. import DialogList from '../../modules/conversation-chat/DialogList.vue';
  140. import DialogInput from '../../modules/conversation-chat/DialogInput.vue';
  141. // import DialogInputAttachedAttrs from '../../modules/conversation-chat/DialogInputAttachedAttrs.vue';
  142. import DialogInputTop from '../../modules/conversation-chat/DialogInputTop.vue';
  143. import DialogFooter from '../../modules/conversation-chat/DialogFooter.vue';
  144. import ChatAsideExpandBtn from '../../modules/conversation-aside/ChatAsideExpandBtn.vue';
  145. import MindMap from '../../components/MindMap.vue';
  146. import ConversationChatMindMapTools from '../../modules/conversation-aside/Conversation-chat-mind-tools.vue';
  147. import ConversationChatWebPage from '../../modules/conversation-aside/Conversation-chat-web-page.vue';
  148. import DialogCommonUsed from '../../modules/conversation-chat/DialogCommonUsed.vue';
  149. import DialogStopSse from '../../modules/conversation-chat/DialogStopSse.vue';
  150. import useWebSocketChat from '../../modules/conversation-dialog/use-websocket-chat';
  151. import useFetchEventSource from '../../modules/conversation-dialog/use-sse-chat';
  152. import {
  153. getHistoryLogs,
  154. getDialogChatId,
  155. uploadFileByAgentInfo,
  156. uploadNewChatInfo,
  157. getMindMapData,
  158. getMapById
  159. } from '../../modules/conversation-dialog/api-dialog';
  160. import formatDialogSendOptions from '../../modules/conversation-dialog/format-dialog-send-options';
  161. import formatDialogSendSseOptions from '../../modules/conversation-dialog/format-dialog-send-sse-options';
  162. import formatDialogHistory from '../../modules/conversation-dialog/format-dialog-history';
  163. import formatDialogQuestionByClient from '../../modules/conversation-dialog/format-dialog-question-by-client';
  164. import formatDialogAnswerForLoading from '../../modules/conversation-dialog/format-dialog-answer-for-loading';
  165. import formatDialogAnswerForException from '../../modules/conversation-dialog/format-dialog-answer-for-exception';
  166. import formatDialogAnswerFromWebsocketReceiveMessage from '../../modules/conversation-dialog/format-dialog-answer-from-websocket_receive_message';
  167. import formatDialogAnswerFromSseReceiveMessage from '../../modules/conversation-dialog/format-dialog-answer-from-Sse_receive_message';
  168. import ConversationChatWordViewer from './ConversationChatWordViewer.vue';
  169. import useEleSizeV1 from '../../utils/use-ele-size-v1';
  170. import useInputClear from './use-input-clear';
  171. import useInputGetOptions from './use-input-get-options';
  172. import useScroll2Bottom from './use-scroll-bottom';
  173. import { IconEye } from '@arco-design/web-vue/es/icon';
  174. import { file, files } from 'jszip';
  175. import { getFileType } from '../../modules/form-dynamic/file-accept';
  176. import { value } from 'lodash-es';
  177. const props = defineProps({
  178. /**
  179. * 智能体的ID
  180. * 此参数新对话中不存在
  181. */
  182. agent_id: String,
  183. /**
  184. * 智能体的一些关键信息
  185. */
  186. CHAT_AGENT_INFOMATION: Object,
  187. /**
  188. * 会话ID
  189. * 如果有此参数,则意味着获取该对话的记录内容
  190. */
  191. session_id: String,
  192. chat_history: String,
  193. /**
  194. * 动态表单数据
  195. * 根据智能体的agent_id 获取到动态表单数据
  196. * */
  197. conversation_params: Object
  198. });
  199. const emit = defineEmits(['updateSessionId']);
  200. const i18n = useI18n();
  201. /**
  202. * input的一些操作方法
  203. */
  204. const inputClearFn1 = useInputClear('dialog_input'); // 主表单清空
  205. const inputClearFn2 = useInputClear('dialog_input_top'); // 顶部附加表单的数据
  206. const getInputTopOptions = useInputGetOptions('dialog_input_top'); // 获取附加表单参数的方法
  207. /**
  208. * 对话区域滚动到底部
  209. */
  210. const scroll2Bottom = useScroll2Bottom('list_scroller');
  211. const scroll2ListBottom = () => {
  212. nextTick(() => {
  213. scroll2Bottom();
  214. });
  215. };
  216. /**
  217. * 整个对话区域的一些尺寸定义
  218. */
  219. const header_height = 30;
  220. const footer_height = 30;
  221. const input_height = ref(100);
  222. /**
  223. * 获取到对话区域的宽度和高度
  224. */
  225. const { width: chat_width, height: chat_height } = useEleSizeV1('chat_scrren_container');
  226. /**
  227. * 对话列表区域的高度
  228. */
  229. const list_height = computed(() => {
  230. return chat_height.value - header_height - footer_height - input_height.value - 20;
  231. });
  232. const input_style = computed(() => {
  233. return {
  234. // height: input_height + 'px'
  235. };
  236. });
  237. const style_header = computed(() => {
  238. return { height: header_height + 'px' };
  239. });
  240. const style_body = computed(() => {
  241. return { height: chat_height - 30 - 30 + 'px' };
  242. });
  243. const style_footer = computed(() => {
  244. return { height: footer_height + 'px' };
  245. });
  246. const style_conversation_chat_content = computed(() => {
  247. if (is_show_right_page.value || show_word_viewer.value) {
  248. return {
  249. width: 55 + '%'
  250. };
  251. } else {
  252. return {
  253. width: 100 + '%'
  254. };
  255. }
  256. });
  257. const style_conversation_chat_right = computed(() => {
  258. if (is_show_right_page.value || show_word_viewer.value) {
  259. return {
  260. width: 35 + '%',
  261. transition: 'width 0.2s ease'
  262. };
  263. } else {
  264. return {
  265. width: 0,
  266. transition: 'width 0.2s ease'
  267. };
  268. }
  269. });
  270. const style_conversation_chat_expand = computed(() => {
  271. if (loading_mind_map.value) {
  272. return {
  273. position: 'relative'
  274. };
  275. } else {
  276. return {
  277. position: ''
  278. };
  279. }
  280. });
  281. const style_conversation_chat_main_container = computed(() => {
  282. if (!show_welcome.value) {
  283. return {
  284. height: 80 + '%',
  285. transition: 'all 0.2s ease'
  286. };
  287. } else {
  288. return {
  289. height: 20 + '%',
  290. transition: 'all 0.2s ease'
  291. };
  292. }
  293. });
  294. const style_conversation_chat_list = computed(() => {
  295. if (is_show_right_page.value || show_word_viewer.value) {
  296. return {
  297. paddingRight: 3 + '%',
  298. paddingLeft: 3 + '%',
  299. boxSizing: 'border-box'
  300. };
  301. } else {
  302. return {
  303. paddingRight: 20 + '%',
  304. paddingLeft: 20 + '%',
  305. boxSizing: 'border-box'
  306. };
  307. }
  308. });
  309. const style_conversation_chat_web_page = computed(() => {
  310. if (!show_mind_map.value) {
  311. return {
  312. height: 100 + '%',
  313. transition: 'all 0.2s ease',
  314. marginTop: 0 + 'px'
  315. };
  316. } else {
  317. return {
  318. marginTop: 20 + 'px',
  319. transition: 'all 0.2s ease',
  320. height: 'calc(100 % - 380px)'
  321. };
  322. }
  323. });
  324. const style_conversation_chat_mind_map = computed(() => {
  325. if (show_mind_map.value) {
  326. if (!isFullScreen.value) {
  327. return {
  328. height: 330 + 'px',
  329. transition: 'all 0.2s ease'
  330. };
  331. } else {
  332. return {
  333. height: window.innerHeight - 90 + 'px',
  334. width: window.innerWidth - 10 + 'px',
  335. position: 'absolute',
  336. top: '0',
  337. left: '0',
  338. zIndex: '9995'
  339. };
  340. }
  341. } else {
  342. return {
  343. display: 'none'
  344. };
  345. }
  346. });
  347. const style_map_error = computed(() => {
  348. if (show_mind_map.value) {
  349. return {
  350. height: 330 + 'px',
  351. transition: 'all 0.2s ease',
  352. display: 'flex'
  353. };
  354. } else {
  355. return {
  356. display: 'none'
  357. };
  358. }
  359. });
  360. // ------------------------------------------------------------------
  361. // ------------------------------------------------------------------
  362. // ------------------------------------------------------------------
  363. /**
  364. * 对话数据列表
  365. */
  366. const chat_log_list = ref([]); // 历史对话的数据列表
  367. const chat_new_list = ref([]); // 新对话的数据列表
  368. const chat_suggestion = ref([]); // 对话推荐项目
  369. /**
  370. * 当前对话使用的ID
  371. * 当用户有输入后,点击提交,此时创建新对话,新对话的ID为此ID
  372. * 之后用户可以一直使用此ID进行对话。
  373. */
  374. const chat_round_id = ref(null);
  375. /**
  376. * 对话发起的次数记录
  377. * 用户每发起一次会话,此值会加1input_disable
  378. */
  379. const chat_round_sent_count = ref(0);
  380. /**
  381. * 新对话,客户端的提问数据
  382. * 用以临时保存客户端的提问数据
  383. * 每当用户确认进行提问时,将新提问赋给此值
  384. * 一旦对话消息通过websocket发送到服务器,此值可清空
  385. */
  386. const chat_new_question = ref({});
  387. /**
  388. * 界面展示的对话列表
  389. * 当用户从历史会话进入时,历史会话记录有数据,当用户基于历史会话进行对话时,新会话也有数据,
  390. * 当用户从agent进入时,仅有新会话,没有历史会话
  391. *
  392. * 页面渲染的会话列表是 历史会话 + 新会话数据
  393. */
  394. const chat_display_list = computed(() => {
  395. return [].concat(chat_log_list.value, chat_new_list.value);
  396. });
  397. /**
  398. * 是否禁用输入组件
  399. */
  400. const input_disabled = ref(false);
  401. /**
  402. * 是否显示停止按钮
  403. * */
  404. const show_stop_sse = ref(false);
  405. /**
  406. * 菜单ID编号
  407. * 1 报表合并
  408. * 2 知识问答
  409. * 3 文档智能
  410. * 4 智能问答
  411. * 5 智能数据
  412. * 6 小数绘图
  413. * 7 文档出卷
  414. * 8 出题组卷
  415. * 9 文档报告
  416. */
  417. const current_menu_id = computed(() => {
  418. const agent = props.CHAT_AGENT_INFOMATION.agent;
  419. const menu_id = agent.menuId / 1;
  420. return menu_id;
  421. });
  422. const current_agent_name = computed(() => {
  423. const agent = props.CHAT_AGENT_INFOMATION.agent;
  424. const name = agent.name || '新会话';
  425. return name;
  426. });
  427. const current_agent_mode = computed(() => {
  428. const agent = props.CHAT_AGENT_INFOMATION.agent;
  429. const mode = agent.mode;
  430. return mode;
  431. });
  432. /**
  433. * 表单区域:是否显示附加参数表单
  434. *
  435. * 根据动态表单返回的数据中user_input_form中是否有数据来控制是否显示附加参数表单
  436. */
  437. const show_input_others_opts = computed(() => {
  438. if (
  439. props.conversation_params &&
  440. props.conversation_params.user_input_form &&
  441. props.conversation_params.user_input_form.length
  442. ) {
  443. return true;
  444. }
  445. return false;
  446. // const menu_id = current_menu_id.value;
  447. // /**
  448. // * 7 文档出卷
  449. // * 9 文档报告
  450. // */
  451. // if (menu_id === 7 || menu_id === 9) {
  452. // return true;
  453. // }
  454. // return false;
  455. });
  456. /**
  457. * 是否支持文本输入
  458. * 返回 true,输入区域将不存在文本输入框
  459. * 返回 false,输入区域将可以进行文本输入
  460. *
  461. *通过conversation_params.retriever_resource.enabled来控制是否支持文本输入
  462. */
  463. const enable_text_input = computed(() => {
  464. let enabled = false;
  465. if (props.conversation_params && props.conversation_params.retriever_resource) {
  466. enabled = props.conversation_params.retriever_resource.enabled;
  467. }
  468. return enabled;
  469. });
  470. /**
  471. * 对话文本是否必须
  472. * 此值如果为 true,则在发起对话之前,会进行 文本的存在性检查
  473. * 此值如果为 false,则在发起对话之前,不会进行 文件存在性检查
  474. */
  475. const is_chat_text_required = computed(() => {
  476. const is_enable = enable_text_input.value;
  477. if (!is_enable) {
  478. return false;
  479. }
  480. return true;
  481. });
  482. /**
  483. * 默认的文本值
  484. */
  485. const default_text_value = computed(() => {
  486. const menu_id = current_menu_id.value;
  487. /**
  488. * 某些情况下,无需输入文本
  489. * 但是在对话部分,需要展示用户的提问,此时可以给一个默认的文本值
  490. * 例如在报表合并情况下,仅需要上传文件,不需要输入文字
  491. */
  492. if (menu_id === 1) {
  493. return '合并Excel';
  494. }
  495. return '';
  496. });
  497. /**
  498. * 表单区域:是否允许文件上传
  499. * 返回 ture,输入区域将会有文件上传按钮
  500. * 返回 false,输入区域将没有文件上传按钮
  501. *
  502. * 根据 conversation_params.file_upload.enabled来控制是否允许文件上传
  503. */
  504. const enable_file_upload = computed(() => {
  505. let enabled = false;
  506. if (props.conversation_params && props.conversation_params.file_upload) {
  507. enabled = props.conversation_params.file_upload.enabled;
  508. }
  509. return enabled;
  510. });
  511. /**
  512. * 对话文件是否必须上传
  513. * 此值如果返回 true,则 在对话时,会进行文件检查
  514. * 此值如果返回 false,则 在对话时,不会检查文件的存在性
  515. */
  516. const is_chat_file_required = computed(() => {
  517. const is_enable = enable_file_upload.value;
  518. if (!is_enable) {
  519. return false;
  520. }
  521. const menu_id = current_menu_id.value;
  522. // 报表合并: 文件必须
  523. if (menu_id === 1) {
  524. // 当第一次会话时,要求必须上传文件
  525. // 非第一次,不做要求
  526. if (chat_round_sent_count.value === 0) {
  527. return true;
  528. }
  529. }
  530. // 文档出卷:文件必须上传
  531. if (menu_id === 7) {
  532. // 当第一次对话时,必须上传文件,
  533. // 非第一次,不做要求
  534. if (chat_round_sent_count.value === 0) {
  535. return true;
  536. }
  537. }
  538. return false;
  539. });
  540. /**
  541. * 此值在 enable_file_upload === true 的前提下有效
  542. * 表单区域:文件上传允许的文件类型
  543. * 对应表单参数的 accept
  544. */
  545. const file_accept = computed(() => {
  546. const menu_id = current_menu_id.value;
  547. /**
  548. * 1 报表合并:上传几个文档,进行合并 允许 .xlsx
  549. * 3 文档智能:上传文档,让智能体进行解析 允许: .pdf/.docx
  550. * 5 智能数据:上传Excel,生成饼状图 允许 .xlsx
  551. * 6 小数绘图:上传图片,描述图片的内容 允许 .png/jpg/jpeg
  552. * 9 文档报告: 允许 .docx
  553. */
  554. if (menu_id === 1) {
  555. return '.xlsx';
  556. }
  557. if (menu_id === 3) {
  558. /**
  559. * 2025-2-8 16:50
  560. * 王永新和赵源在测试deepseek时发现 文档智能 能上传的文件类型有限
  561. * 要求将accept调整为和dify的 文档 类型一致
  562. */
  563. // return '.pdf,.docx';
  564. return '.txt,.md,.markdown,.pdf,.html,.xlsx,.xls,.docx,.csv,.eml,.msg,.pptx,.ppt,.xml,.epub';
  565. }
  566. // 智能数据
  567. if (menu_id === 5) {
  568. return '.xlsx,.xls,.csv';
  569. }
  570. if (menu_id === 6) {
  571. return '.jpg,.jpeg,.png';
  572. }
  573. if (menu_id === 9) {
  574. return '.docx';
  575. }
  576. // return '*';
  577. return '.txt,.md,.markdown,.pdf,.html,.xlsx,.xls,.docx,.csv,.eml,.msg,.pptx,.ppt,.xml,.epub.jpg,.jpeg,.png';
  578. });
  579. /**
  580. * 答案可用的操作按钮
  581. * 可用的值有 [] 或者 ['copy']
  582. * 后续可能有新值添加
  583. */
  584. const answer_action_list = computed(() => {
  585. const menu_id = current_menu_id.value;
  586. // 报表合并无操作按钮
  587. if (menu_id === 1) {
  588. return [];
  589. }
  590. return ['copy', 'retry', 'edit', 'download'];
  591. });
  592. /**
  593. * 控制对话列表滚动条展示
  594. * 当右侧有页面打开,隐藏滚动条
  595. *
  596. */
  597. const show_right = computed(() => {
  598. return is_show_right_page.value || show_word_viewer.value;
  599. });
  600. /**
  601. * 内容分享操作
  602. * @param chat_id
  603. */
  604. const handleChatShare = (chat_id) => {};
  605. const show_word_viewer = ref(false);
  606. // 要编辑的文本内容
  607. const edit_txt_value = ref('');
  608. const closeChatWord = () => {
  609. show_word_viewer.value = false;
  610. };
  611. // 更新输入框中的文本
  612. const ask_input_text = ref('');
  613. //
  614. const updatAskTxt = (txt) => {
  615. ask_input_text.value = txt;
  616. };
  617. const dialog_input = ref(null);
  618. const quickSend = (txt) => {
  619. ask_input_text.value = txt;
  620. const config = dialog_input.value.options.config;
  621. handleInputSubmit({
  622. config,
  623. text: txt,
  624. files: []
  625. });
  626. };
  627. /**
  628. * 编辑文本
  629. * */
  630. const handleEditChat = (item) => {
  631. // edit_txt_value.value = marked(item.answer || '');
  632. edit_txt_value.value = item.answer;
  633. is_show_right_page.value = false;
  634. show_word_viewer.value = true;
  635. };
  636. const retry_action_option = ref(null);
  637. const handleActionConfigChange = (config) => {
  638. retry_action_option.value = config;
  639. };
  640. /**
  641. * 重新生成
  642. * */
  643. const handleRetryChat = async (item) => {
  644. //根据message_id查找对应的问题
  645. const question_idx = chat_display_list.value.findIndex(
  646. (it) => it.client_custom_chat_message_id === item.client_custom_chat_message_id
  647. );
  648. if (question_idx != -1) {
  649. const question_item = chat_display_list.value[question_idx - 1];
  650. let chatMode = retry_action_option.value?.chatMode || 1;
  651. let isDeep = retry_action_option.value?.isDeep || 1;
  652. if (question_item.client_custom_upload_file_list?.length) {
  653. chatMode = 1;
  654. }
  655. if (question_item.client_custom_knowledge_id?.length) {
  656. chatMode = 3;
  657. }
  658. const options = {
  659. files: question_item.client_custom_upload_file_list || [],
  660. text: question_item.question,
  661. config: {
  662. knowledgeId: question_item.client_custom_knowledge_id || [],
  663. isDeep,
  664. chatMode
  665. }
  666. };
  667. toSubmitNewChat(options);
  668. }
  669. };
  670. // 编辑问题重新问答
  671. const handleSendChat = async (item) => {
  672. let chatMode = retry_action_option.value?.chatMode || 1;
  673. let isDeep = retry_action_option.value?.isDeep || 1;
  674. let parent_id = '';
  675. const idx = chat_display_list.value.findIndex((it) => it.client_custom_unique_id === item.client_custom_unique_id);
  676. if (idx !== -1 && idx > 0) {
  677. parent_id = chat_display_list.value[idx - 1].parent_id || '';
  678. }
  679. if (item.client_custom_upload_file_list?.length) {
  680. chatMode = 1;
  681. }
  682. if (item.client_custom_knowledge_id?.length) {
  683. chatMode = 3;
  684. }
  685. if (item.client_custom_upload_file_list?.length) {
  686. chatMode = 1;
  687. }
  688. if (item.client_custom_knowledge_id?.length) {
  689. chatMode = 3;
  690. }
  691. const options = {
  692. files: item.client_custom_upload_file_list || [],
  693. text: item.content,
  694. parent_id,
  695. config: {
  696. knowledgeId: item.client_custom_knowledge_id || [],
  697. isDeep,
  698. chatMode
  699. }
  700. };
  701. toSubmitNewChat(options);
  702. };
  703. /**
  704. * 在聊天界面点击推荐项
  705. * 继续对话
  706. */
  707. const handleSuggestSubmit = (suggest) => {};
  708. /**
  709. * 处理页面中的错误消息
  710. * @param msg
  711. */
  712. const handleErrorMessage = (msg) => {
  713. Message.error({ content: msg, position: 'top' });
  714. };
  715. // ---------------------------------------------
  716. /**
  717. * 异常处理
  718. * 异常处理过程是一个会话模拟的过程
  719. * 先在界面上显示加载动画,然后紧接着显示错误提示
  720. * 让用户感知到智能体有处理数据的过程
  721. * 所以在异常处理阶段,有一些需要处理的关键点,比如欢迎界面的显示(如果在显示,则需要隐藏,否则对话显示不了)
  722. * @param exceptionInfo
  723. */
  724. const handleChatRequestionException = (exceptionInfo) => {
  725. // 已结束
  726. const answerData = formatDialogAnswerForException(props.CHAT_AGENT_INFOMATION, exceptionInfo);
  727. // 原来的最后一项弹出
  728. chat_new_list.value.pop();
  729. // 添加新的最后一项
  730. chat_new_list.value.push(answerData);
  731. // 对话已结束:文件上传、文本输入区域放开使用
  732. input_disabled.value = false;
  733. // 对话区域滚动到底部
  734. scroll2ListBottom();
  735. };
  736. /**
  737. * 输入区域的尺寸发生了变化
  738. * 整个对话区域的尺寸也需要依据此变化而变化
  739. * @param size
  740. */
  741. const handleInputSizeChange = (size) => {
  742. const input_width = size.width;
  743. // const input_height = size.height;
  744. setTimeout(() => {
  745. input_height.value = size.height;
  746. });
  747. };
  748. /**
  749. * 是否显示欢迎页
  750. * 当用户进入对话页面,还未正式开始对话时
  751. * 对话区域会显示欢迎内容
  752. * 当用户开始对话时,欢迎内容消失,对话列表开始展示
  753. */
  754. const show_welcome = ref(false);
  755. const mindMap_str = ref('');
  756. /**
  757. * mindMap组件对象
  758. *
  759. * */
  760. const mind_map = ref();
  761. /**
  762. * mindMap 显示的内容
  763. *
  764. * */
  765. const mindMap_content = ref('');
  766. const mindMap_err = ref('');
  767. /**
  768. * 图谱的查询 文本字段
  769. * */
  770. const mindMap_txt = ref('');
  771. const minMap_messageId = ref('');
  772. const fullScreen_mindMap = ref(null);
  773. const isFullScreen = ref(false);
  774. /**
  775. * mindMap 盒子的尺寸
  776. * */
  777. const box_style = computed(() => {
  778. if (is_show_right_page.value) {
  779. return {
  780. width: '100%',
  781. height: 'calc(100% - 30px)'
  782. };
  783. }
  784. });
  785. /**
  786. * mindMap 导图实际尺寸的尺寸
  787. * */
  788. const svg_style = computed(() => {
  789. if (is_show_right_page.value) {
  790. return {
  791. width: '100%',
  792. height: '100%'
  793. };
  794. }
  795. });
  796. /**
  797. * mindMap 展开按钮事件
  798. * node:当前node数据
  799. * full:全部node数据
  800. * el:当前点击的元素
  801. * */
  802. const extendMindMap = async (item) => {
  803. const { node, full, el } = item;
  804. mindMap_txt.value = decodeHtmlCharCodes(node.content);
  805. getMindMapData({ messageId: minMap_messageId.value, query: mindMap_txt.value }).then((res) => {
  806. if (res.code / 1 === 200) {
  807. mindMap_str.value = res.data.mindmap;
  808. mindMap_content.value = res.data.mindmap;
  809. } else {
  810. mindMap_content.value = mindMap_str.value;
  811. }
  812. });
  813. };
  814. // 转码函数 展开节点用到
  815. function decodeHtmlCharCodes(str) {
  816. const textarea = document.createElement('textarea');
  817. textarea.innerHTML = str;
  818. return textarea.value;
  819. }
  820. const is_show_right_page = ref(false); //控制右侧页面展示显示
  821. const show_mind_map = ref(false); //控制右侧思维导图是否显示
  822. const loading_mind_map = ref(false); //初始控制思维导图父盒子显示加载中
  823. const show_web_page = ref(false); //控制网页搜索展示
  824. // 关闭网页搜索
  825. const closeWebPage = () => {
  826. show_web_page.value = false;
  827. is_show_right_page.value = false;
  828. };
  829. // 展示右侧页面按钮
  830. // const show_right_page_btn = computed(() => {
  831. // if (chat_display_list.value.length) {
  832. // const last_item = chat_display_list.value.at(-1);
  833. // if (last_item.client_custom_item_internet && last_item.client_custom_item_internet_list.length) {
  834. // return true;
  835. // }
  836. // }
  837. // return false;
  838. // });
  839. //控制右侧顶部按钮开关
  840. const show_right_page_btn = ref(false);
  841. /**
  842. * 控制右侧显示/隐藏
  843. * 展开 永远显示最后一项的网页和思维导图,如果二者都没有则,不显示
  844. * */
  845. const toggleRight = () => {
  846. is_show_right_page.value = !is_show_right_page.value;
  847. if (is_show_right_page.value) {
  848. const last = chat_display_list.value.at(-1);
  849. if (last.client_custom_item_internet_list.length) {
  850. show_web_page.value = true;
  851. search_list.value = last.client_custom_item_internet_list;
  852. }
  853. if (last.client_custom_item_mind_map) {
  854. // show_mind_map.value = true;
  855. toShowMindMap({ client_custom_chat_message_id: last.client_custom_chat_message_id });
  856. // getMindMapData({ messageId: last.client_custom_chat_message_id.value, query: mindMap_txt.value }).then((res) => {
  857. // if (res.code / 1 === 200) {
  858. // setTimeout(() => {
  859. // mindMap_str.value = res.data.mindmap;
  860. // mindMap_content.value = res.data.mindmap;
  861. // }, 200);
  862. // }
  863. // });
  864. }
  865. } else {
  866. show_mind_map.value = false;
  867. show_web_page.value = false;
  868. }
  869. };
  870. /**
  871. * 放大
  872. * */
  873. const zoomIn = () => {
  874. mind_map.value.zoomIn();
  875. };
  876. /**
  877. * 缩小
  878. * */
  879. const zoomOut = () => {
  880. mind_map.value.zoomOut();
  881. };
  882. /**
  883. * 自适应
  884. * */
  885. const zoomReset = () => {
  886. mind_map.value.zoomReset();
  887. };
  888. const original_obj = ref({
  889. height: '',
  890. width: '',
  891. position: '',
  892. zIndex: ''
  893. }); // 用来存储原始高度
  894. /**
  895. * 全屏
  896. * */
  897. const toFullScreen = () => {
  898. if (isFullScreen.value) {
  899. return;
  900. }
  901. // 动态设置div高度为屏幕可见高度(排除浏览器头部)
  902. if (fullScreen_mindMap.value) {
  903. isFullScreen.value = true;
  904. // 存储原始高度,进入全屏时恢复
  905. original_obj.value.height = fullScreen_mindMap.value.style.height;
  906. original_obj.value.width = fullScreen_mindMap.value.style.width;
  907. original_obj.value.position = fullScreen_mindMap.value.style.position;
  908. original_obj.value.zIndex = fullScreen_mindMap.value.style.zIndex;
  909. console.log(original_obj.value);
  910. // 设置div高度为视口高度,避免覆盖浏览器头部
  911. // fullScreen_mindMap.value.style.removeProperty('height');
  912. fullScreen_mindMap.value.style.height = window.innerHeight - 90 + 'px';
  913. fullScreen_mindMap.value.style.width = window.innerWidth - 10 + 'px';
  914. fullScreen_mindMap.value.style.position = 'absolute';
  915. fullScreen_mindMap.value.style.top = '0';
  916. fullScreen_mindMap.value.style.left = '0';
  917. fullScreen_mindMap.value.style.zIndex = '9995';
  918. console.log(fullScreen_mindMap.value.style.height);
  919. // zoomReset();
  920. }
  921. };
  922. // 退出全屏模式的函数
  923. const exitFullScreen = () => {
  924. // 恢复原始高度
  925. if (fullScreen_mindMap.value) {
  926. fullScreen_mindMap.value.style.height = original_obj.value.height;
  927. fullScreen_mindMap.value.style.width = original_obj.value.width;
  928. fullScreen_mindMap.value.style.position = original_obj.value.position;
  929. fullScreen_mindMap.value.style.zIndex = original_obj.value.zIndex;
  930. isFullScreen.value = false;
  931. zoomReset();
  932. }
  933. };
  934. /**
  935. * 下载
  936. * */
  937. const downloadMap = () => {
  938. mind_map.value.downloadPNG();
  939. };
  940. /**
  941. * 打开欢迎页面
  942. * */
  943. const toShowWelcome = () => {
  944. toNewChat();
  945. show_welcome.value = true;
  946. chat_display_list.value = [];
  947. chat_log_list.value = [];
  948. chat_new_list.value = [];
  949. is_show_right_page.value = false;
  950. };
  951. const toNewChat = () => {
  952. if (is_init_model.value) {
  953. pushInitAnswer();
  954. }
  955. is_init_model.value = false;
  956. chat_display_list.value = [];
  957. if (!show_welcome.value) {
  958. return;
  959. }
  960. show_welcome.value = false;
  961. pushInitAnswer();
  962. };
  963. const pushInitAnswer = () => {
  964. chat_log_list.value.push({
  965. answer: 'Hi,我是Smart~ <br> 很高兴遇见你!你可以随时把网址或文件发给我,我来帮你看看',
  966. client_custom_chat_status: '2',
  967. client_custom_chat_type: '0',
  968. client_custom_item_type: 'a',
  969. client_custom_item_think: false, // 是否有思考
  970. client_custom_item_think_content: '', //思考内容
  971. client_custom_item_internet: false, //是否联网
  972. client_custom_item_internet_list: [], //联网内容
  973. client_custom_chat_message_id: '' //会话的message_id
  974. });
  975. };
  976. /**
  977. * 智能体中区分是首次进去展示动态表单,还是点击按钮进去展示动态表单
  978. * */
  979. const is_init_model = ref(false);
  980. const initModel = (val) => {
  981. is_init_model.value = val;
  982. };
  983. const target_action_config = ref({});
  984. /**
  985. * 初始化sse相关方法
  986. * */
  987. const { startStream, stopStream } = useFetchEventSource();
  988. /**
  989. * 输入已经确认要提交了
  990. *
  991. * 1. 创建会话 get-chat-id
  992. * 2. 文件上传 如果有文件,则需要文件上传,没有文件,则不需要文件上传
  993. * 3. 连接socket,socket 发送文本与文件数据内容
  994. * 4. 接收 socket 返回的数据项
  995. */
  996. const handleInputSubmit = async (options) => {
  997. show_welcome.value = false;
  998. if (!chat_display_list.value.length) {
  999. pushInitAnswer();
  1000. }
  1001. if (!props.agent_id) {
  1002. toSubmitNewChat(options);
  1003. return;
  1004. }
  1005. let ask_text = options.text; // 文本
  1006. const ask_files = options.files; // 文件列表
  1007. const action_config = options.config; //是否深度思考,是否联网,是否选择知识库
  1008. target_action_config.value = {};
  1009. // 用户选定的文件数量
  1010. const file_total = ask_files.length;
  1011. const is_text_required = is_chat_text_required.value;
  1012. const is_file_required = is_chat_file_required.value;
  1013. // 检查对话文字是否必须
  1014. if (is_text_required) {
  1015. if (!ask_text) {
  1016. const msg = i18n.t('conversation.please_input_text');
  1017. handleErrorMessage(msg);
  1018. return;
  1019. }
  1020. } else {
  1021. ask_text = default_text_value.value;
  1022. }
  1023. // 检查对话文件是否必须
  1024. if (is_file_required) {
  1025. if (file_total === 0) {
  1026. const msg = i18n.t('conversation.please_upload_file');
  1027. handleErrorMessage(msg);
  1028. return;
  1029. }
  1030. }
  1031. // --------------------------------------------
  1032. // --------------------------------------------
  1033. // --------------------------------------------
  1034. // --------------------------------------------
  1035. // --------------------------------------------消息发送前的准备工作
  1036. // 保存附加参数数据,由于下方有一个 inputClearFn2() 方法的执行
  1037. // 会将所有数据清空,故附件参数需要在清理之前保存
  1038. const other_options = getInputTopOptions();
  1039. /**
  1040. * 当新消息发送时
  1041. * 意味着新一轮对话的开始
  1042. * 此时应该将之前一轮对话的信息进行清空
  1043. *
  1044. * 整理客户端提问的数据
  1045. * 组装Question数据
  1046. */
  1047. const client_question = formatDialogQuestionByClient(ask_text, ask_files);
  1048. // 保存用户的提问信息
  1049. // 当消息正式发出后,将提问数据放入列表中
  1050. chat_new_question.value = client_question;
  1051. // 消息即将发送,记录用户的提问数据
  1052. chat_new_list.value.push(chat_new_question.value);
  1053. // 客户端答案数据(loading状态),客户端模拟的答案数据,依据此数据展示加载中的动画
  1054. const answerLoadingMsg = formatDialogAnswerForLoading(props.CHAT_AGENT_INFOMATION);
  1055. chat_new_list.value.push(answerLoadingMsg);
  1056. /**
  1057. * 会话已确认发送
  1058. * 隐藏欢迎内容,显示对话内容
  1059. */
  1060. show_welcome.value = false;
  1061. // 清空文本数据和文件数据,避免下一次会话冲突
  1062. // 清空表单里的数据
  1063. inputClearFn1();
  1064. inputClearFn2();
  1065. // 清空可能存在的推荐项目
  1066. chat_suggestion.value = [];
  1067. // 清空已保存的提问数据(提问数据已经推入数组,此处无需继续保存)
  1068. chat_new_question.value = null;
  1069. // 文件上传、文本输入区域禁用
  1070. input_disabled.value = true;
  1071. // 对话区域滚动到底部
  1072. scroll2ListBottom();
  1073. // --------------------------------------------
  1074. // --------------------------------------------
  1075. // --------------------------------------------
  1076. // --------------------------------------------
  1077. // --------------------------------------------
  1078. const agent_id = props.agent_id;
  1079. // 对话分为三个步骤
  1080. // 1. 创建会话,如果已经创建,则不需要再次创建
  1081. // 2. 上传文件,如果不存在文件,则无需文件上传
  1082. // 3. 连接websocket
  1083. /**
  1084. * 创建会话 get chat id
  1085. * 判断对话是否已经创建
  1086. * 如果对话已经创建,则使用已经创建的对话
  1087. * 否则,创建新对话
  1088. */
  1089. if (chat_round_id.value === null) {
  1090. const session_id = props.session_id;
  1091. if (session_id) {
  1092. // 已有会话ID,使用此会话ID
  1093. chat_round_id.value = session_id;
  1094. } else {
  1095. // 无会话ID,则是首页会话
  1096. if (!agent_id) {
  1097. return;
  1098. }
  1099. // 未创建chat,开始创建
  1100. try {
  1101. const chatResult = await getDialogChatId(agent_id);
  1102. if (chatResult.code / 1 === 200) {
  1103. // 成功
  1104. const res_data = chatResult.data;
  1105. chat_round_id.value = res_data.chat_id;
  1106. } else {
  1107. // 失败
  1108. // 处理异常:直接在界面上显示
  1109. handleChatRequestionException({
  1110. message: `code: ${chatResult.code} message: ${chatResult.msg}`
  1111. });
  1112. return;
  1113. }
  1114. } catch (err) {
  1115. console.log('err-nfsdnakiuwirea7', err);
  1116. // 处理错误
  1117. handleChatRequestionException({
  1118. message: err.message
  1119. });
  1120. return;
  1121. }
  1122. }
  1123. } else {
  1124. // 已创建chat,使用已创建的会话ID,进入下一步流程
  1125. }
  1126. /**
  1127. * 查看动态表单中是否有文件需要上传,如果有,则需要先上传文件
  1128. * */
  1129. if (other_options && other_options.files) {
  1130. const files_obj = other_options.files;
  1131. const chat_id = chat_round_id.value;
  1132. const menu_id = current_menu_id.value;
  1133. for (const key in files_obj) {
  1134. const upload_files = files_obj[key].map((item) => item.file);
  1135. const uploadResult = await uploadFileByAgentInfo(agent_id, chat_id, upload_files, menu_id);
  1136. if (uploadResult && uploadResult.length) {
  1137. files_obj[key] = files_obj[key].map((file) => {
  1138. const uploadedFile = uploadResult.find((uploaded) => uploaded.name === file.name);
  1139. // 如果找到了匹配的文件,则回填 id
  1140. if (uploadedFile) {
  1141. file.upload_file_id = uploadedFile.id;
  1142. file.url = '';
  1143. }
  1144. return file;
  1145. });
  1146. // 判断如果对应的文件是单文件上传,则显示为对象
  1147. if (files_obj[key].some((item) => item.limit_1)) {
  1148. files_obj[key] = files_obj[key][0];
  1149. }
  1150. }
  1151. }
  1152. other_options.options = {
  1153. ...other_options.options,
  1154. ...files_obj
  1155. };
  1156. }
  1157. // 文件上传后的信息
  1158. let files_response = null;
  1159. /**
  1160. * 如果存在文件数据,则进行文件上传
  1161. */
  1162. if (file_total !== 0) {
  1163. // 进行文件上传,获取文件信息
  1164. const chat_id = chat_round_id.value;
  1165. if (agent_id) {
  1166. try {
  1167. const menu_id = current_menu_id.value;
  1168. const uploadResult = await uploadFileByAgentInfo(agent_id, chat_id, ask_files, menu_id);
  1169. if (uploadResult.length) {
  1170. files_response = uploadResult.map((item) => {
  1171. return {
  1172. ...item,
  1173. url: '',
  1174. type: getFileType(item),
  1175. transfer_method: 'local_file',
  1176. upload_file_id: item.id
  1177. };
  1178. });
  1179. }
  1180. // if (uploadResult.code / 1 === 200) {
  1181. // // 文件上传成功
  1182. // files_response = uploadResult.data;
  1183. // } else {
  1184. // // 文件上传异常
  1185. // handleChatRequestionException({
  1186. // message: `code: ${uploadResult.code} \n\n message: ${uploadResult.msg}`
  1187. // });
  1188. // return;
  1189. // }
  1190. } catch (err) {
  1191. console.log('err-fhskafhakfjslkfjl', err);
  1192. handleChatRequestionException({
  1193. message: err.message
  1194. });
  1195. return;
  1196. }
  1197. } else {
  1198. }
  1199. }
  1200. // TODO: 重新组装sse参数,调用sse接口问答
  1201. const query_params = {
  1202. agent_id,
  1203. mode: current_agent_mode.value, //智能体类型
  1204. query: ask_text,
  1205. files: files_response || [],
  1206. inputs: other_options ? other_options.options : [],
  1207. session_id: props.session_id || ''
  1208. };
  1209. startStream(query_params, sseOnMessage);
  1210. return;
  1211. };
  1212. // 首页对话
  1213. const toSubmitNewChat = async (options) => {
  1214. // 每次问答时,先隐藏右侧按钮
  1215. show_right_page_btn.value = false;
  1216. is_show_right_page.value = false;
  1217. let ask_text = options.text; // 文本
  1218. const ask_files = options.files; // 文件列表
  1219. const action_config = options.config; //是否深度思考,是否联网,是否选择知识库,是否生成思维导图
  1220. target_action_config.value = options.config;
  1221. let parent_id = '';
  1222. if (chat_display_list.value.length) {
  1223. const lastItem = chat_display_list.value[chat_display_list.value.length - 1];
  1224. if (lastItem.parent_id) {
  1225. parent_id = lastItem.parent_id;
  1226. }
  1227. }
  1228. if (options.parent_id) {
  1229. parent_id = options.parent_id;
  1230. }
  1231. /**
  1232. *判断是否要生成思维导图
  1233. * 如果是生成思维导图,则将文本内容保存到mindMap_txt中,用于生成思维导图
  1234. * */
  1235. if (target_action_config.value.mind_map) {
  1236. mindMap_txt.value = options.text;
  1237. } else {
  1238. mindMap_txt.value = '';
  1239. show_mind_map.value = false;
  1240. }
  1241. // 用户选定的文件数量
  1242. const file_total = ask_files.length;
  1243. if (!ask_text) {
  1244. const msg = i18n.t('conversation.please_input_text');
  1245. handleErrorMessage(msg);
  1246. return;
  1247. }
  1248. const other_options = getInputTopOptions();
  1249. const knowledgeIdList = target_action_config.value?.knowledgeId || [];
  1250. const client_question = formatDialogQuestionByClient(ask_text, ask_files, knowledgeIdList);
  1251. // 保存用户的提问信息
  1252. // 当消息正式发出后,将提问数据放入列表中
  1253. chat_new_question.value = client_question;
  1254. // 消息即将发送,记录用户的提问数据
  1255. chat_new_list.value.push(chat_new_question.value);
  1256. // 客户端答案数据(loading状态),客户端模拟的答案数据,依据此数据展示加载中的动画
  1257. const answerLoadingMsg = formatDialogAnswerForLoading(props.CHAT_AGENT_INFOMATION);
  1258. chat_new_list.value.push(answerLoadingMsg);
  1259. inputClearFn1();
  1260. inputClearFn2();
  1261. // 清空可能存在的推荐项目
  1262. chat_suggestion.value = [];
  1263. // 清空已保存的提问数据(提问数据已经推入数组,此处无需继续保存)
  1264. chat_new_question.value = null;
  1265. // 文件上传、文本输入区域禁用
  1266. input_disabled.value = true;
  1267. // 对话区域滚动到底部
  1268. scroll2ListBottom();
  1269. // 文件上传后的信息
  1270. let files_response = null;
  1271. // 判断是否有要上传的文件,如果有上传文件,否则组合参数进行对话
  1272. if (file_total !== 0) {
  1273. try {
  1274. const uploadResult = await uploadNewChatInfo(1, ask_files);
  1275. files_response = uploadResult.map((item) => {
  1276. return {
  1277. ...item,
  1278. url: '',
  1279. type: getFileType(item),
  1280. transfer_method: 'local_file',
  1281. upload_file_id: item.id
  1282. };
  1283. });
  1284. } catch (e) {
  1285. handleChatRequestionException({
  1286. message: e.message
  1287. });
  1288. }
  1289. }
  1290. // 组合参数
  1291. const query_params = {
  1292. query: ask_text,
  1293. files: files_response,
  1294. session_id: props.session_id || '',
  1295. knowledgeId: action_config.knowledgeId,
  1296. chatMode: action_config.chatMode,
  1297. isDeep: action_config.isDeep,
  1298. parentId: parent_id
  1299. };
  1300. startStream(query_params, sseOnMessage);
  1301. return;
  1302. };
  1303. /**
  1304. * 网页数据
  1305. * */
  1306. const search_list = ref([]);
  1307. const toSearchPage = (item) => {
  1308. search_list.value = [...item.client_custom_item_internet_list];
  1309. // minMap_messageId.value = item.client_custom_chat_message_id;
  1310. if (!is_show_right_page.value) {
  1311. is_show_right_page.value = true;
  1312. // if (!mindMap_content.value) {
  1313. // mindMap_err.value = '暂无内容';
  1314. // }
  1315. // toShowMindMap(item);
  1316. }
  1317. if (!show_web_page.value) {
  1318. show_web_page.value = true;
  1319. }
  1320. };
  1321. const toShowMindMap = async (item) => {
  1322. const res = await getHistoryMapData(item.client_custom_chat_message_id);
  1323. if (res.code / 1 === 200) {
  1324. if (res.data.mindmap) {
  1325. mindMap_content.value = res.data.mindmap;
  1326. mindMap_err.value = '';
  1327. } else {
  1328. mindMap_content.value = '';
  1329. mindMap_err.value = '暂无内容';
  1330. }
  1331. } else {
  1332. mindMap_content.value = '';
  1333. mindMap_err.value = '暂无内容';
  1334. }
  1335. is_show_right_page.value = true;
  1336. minMap_messageId.value = item.client_custom_chat_message_id;
  1337. setTimeout(() => {
  1338. show_mind_map.value = true;
  1339. }, 200);
  1340. };
  1341. /**
  1342. * 生成思维导图的函数
  1343. *
  1344. * */
  1345. const toGenerateMindMap = async () => {
  1346. // is_show_right_page.value = false;
  1347. loading_mind_map.value = true;
  1348. getMindMapData({ messageId: minMap_messageId.value, query: mindMap_txt.value })
  1349. .then((res) => {
  1350. if (res.code / 1 === 200) {
  1351. mindMap_str.value = res.data.mindmap;
  1352. mindMap_content.value = res.data.mindmap;
  1353. mindMap_err.value = '';
  1354. // 判断如果生成思维导图显示右侧按钮
  1355. show_right_page_btn.value = true;
  1356. show_right_page_btn.value = true;
  1357. if (!res.data.mindmap) {
  1358. mindMap_err.value = '文本内容太少,不足以生成思维导图';
  1359. }
  1360. } else {
  1361. mindMap_err.value = '文本内容太少,不足以生成思维导图';
  1362. }
  1363. })
  1364. .catch((e) => {
  1365. mindMap_err.value = '文本内容太少,不足以生成思维导图';
  1366. })
  1367. .finally(() => {
  1368. loading_mind_map.value = false;
  1369. // setTimeout(() => {
  1370. // show_mind_map.value = true;
  1371. // }, 200);
  1372. });
  1373. };
  1374. /**
  1375. * sseOnMessage处理函数
  1376. * */
  1377. const sseOnMessage = (ev) => {
  1378. // return;
  1379. // sse流返回的数据
  1380. const res_data = JSON.parse(ev.data);
  1381. if (res_data.session_id) {
  1382. emit('updateSessionId', res_data.session_id);
  1383. }
  1384. if (res_data.message_id) {
  1385. minMap_messageId.value = res_data.message_id;
  1386. }
  1387. //sse流式结束
  1388. if (res_data.event === 'message_end') {
  1389. stopStream();
  1390. if (props.agent_id) {
  1391. // return;
  1392. } else {
  1393. // 如果mimndMap_txt有值,则生成思维导图
  1394. res_data.isMindMap = target_action_config.value.mind_map;
  1395. if (mindMap_txt.value) {
  1396. toGenerateMindMap();
  1397. } else {
  1398. }
  1399. }
  1400. }
  1401. if (res_data.event === 'node_finished' && res_data.data.title && res_data.data.title === 'SearXNG 搜索') {
  1402. if (res_data.data.outputs?.json) {
  1403. search_list.value = res_data.data.outputs.json;
  1404. if (search_list.value.length) {
  1405. // 当有网页时,显示右侧按钮
  1406. show_right_page_btn.value = true;
  1407. show_web_page.value = true;
  1408. is_show_right_page.value = true;
  1409. }
  1410. }
  1411. }
  1412. if (res_data.event === 'error' || res_data.message === 'error') {
  1413. //sse错误,显示错误信息
  1414. handleChatRequestionException({
  1415. message: res_data.error
  1416. });
  1417. return;
  1418. }
  1419. // TODO: 将返回数据填充到页面
  1420. // 获取当前对话中最后一个对话数据
  1421. // 最后一个对话数据为当前正在进行的对话
  1422. const last_msg = chat_new_list.value[chat_new_list.value.length - 1];
  1423. /**
  1424. * 要混入的回答参数
  1425. * client_custom_item_think: false, // 是否有思考
  1426. * client_custom_item_think_content: '', //思考内容
  1427. * client_custom_item_internet: false, //是否联网
  1428. * client_custom_item_internet_list: [], //联网内容
  1429. * client_custom_chat_message_id: '' //会话的message_id
  1430. *
  1431. */
  1432. if (Object.keys(target_action_config.value)) {
  1433. res_data.isDeep = target_action_config.value.isDeep / 1 === 2 ? true : false;
  1434. res_data.isInternet = target_action_config.value.chatMode / 1 === 2 ? true : false;
  1435. res_data.isMindMap = target_action_config.value.mind_map;
  1436. }
  1437. /**
  1438. * 整理返回的数据
  1439. *
  1440. */
  1441. const currentChat = formatDialogAnswerFromSseReceiveMessage(res_data, last_msg, props.CHAT_AGENT_INFOMATION);
  1442. const chat_status = currentChat.status;
  1443. // 处理对话数据
  1444. const handleChatData = () => {
  1445. const chat_data = currentChat.data; // 对话相关的数据项
  1446. // 原来的最后一项弹出
  1447. chat_new_list.value.pop();
  1448. // 添加新的最后一项
  1449. chat_new_list.value.push(chat_data);
  1450. // 滚动到底部
  1451. scroll2ListBottom();
  1452. };
  1453. if (chat_status === 0) {
  1454. // 未开始: 此状态不存在,无需处理
  1455. }
  1456. if (chat_status === 1) {
  1457. // 此轮对话仍然在进行中
  1458. handleChatData();
  1459. show_stop_sse.value = true;
  1460. }
  1461. if (chat_status === 2) {
  1462. // 此轮对话已结束
  1463. handleChatData();
  1464. // 对话已结束:检查是否存在推荐数据
  1465. const chat_suggest = currentChat.suggest; // 对话相关的推荐数据
  1466. if (chat_suggest) {
  1467. chat_suggestion.value = chat_suggest;
  1468. }
  1469. // 对话已结束:文件上传、文本输入区域放开使用
  1470. input_disabled.value = false;
  1471. show_stop_sse.value = false;
  1472. }
  1473. if (chat_status === 3) {
  1474. // 此轮对话出错了
  1475. // TODO: 错误处理
  1476. // 此轮对话已结束
  1477. handleChatData();
  1478. // 对话已结束:文件上传、文本输入区域放开使用
  1479. input_disabled.value = false;
  1480. show_stop_sse.value = false;
  1481. }
  1482. };
  1483. // 停止响应
  1484. const stopResponse = () => {
  1485. stopStream();
  1486. show_stop_sse.value = false;
  1487. input_disabled.value = false;
  1488. // 停止后修改最后一项的状态为已结束
  1489. const lastItem = chat_new_list.value.pop();
  1490. lastItem.client_custom_chat_status = 2;
  1491. chat_new_list.value.push(lastItem);
  1492. scroll2ListBottom();
  1493. };
  1494. const toShowRightBtn = () => {
  1495. if (chat_display_list.value.length) {
  1496. const last_item = chat_display_list.value.at(-1);
  1497. console.log(last_item);
  1498. if (last_item.client_custom_item_mind_map) {
  1499. show_right_page_btn.value = true;
  1500. }
  1501. if (last_item.client_custom_item_internet && last_item.client_custom_item_internet_list.length) {
  1502. show_right_page_btn.value = true;
  1503. }
  1504. }
  1505. };
  1506. watch(
  1507. () => style_conversation_chat_mind_map.value,
  1508. () => {
  1509. if (mind_map.value) {
  1510. setTimeout(() => {
  1511. zoomReset();
  1512. }, 30);
  1513. }
  1514. },
  1515. {
  1516. deep: true
  1517. }
  1518. );
  1519. watch(
  1520. () => props.agent_id,
  1521. () => {
  1522. if (props.agent_id) {
  1523. show_welcome.value = false;
  1524. }
  1525. }
  1526. );
  1527. watch(
  1528. () => props.session_id,
  1529. () => {
  1530. if (props.session_id) {
  1531. /**
  1532. * 当页面中有记录,则不调用历史记录
  1533. * 防止在新会话中每次问答后更新session_id去调用历史记录,
  1534. * */
  1535. if (chat_display_list.value.length) {
  1536. return;
  1537. }
  1538. // 有历史记录:获取历史记录内容进行展示
  1539. getHistoryLogs(props.session_id)
  1540. .then(async (res) => {
  1541. if (res.code / 1 === 200) {
  1542. const res_data = res.data;
  1543. // 成功
  1544. const session_list = res_data.message;
  1545. // for (let key in session_list) {
  1546. // if (session_list[key].role == 'assistant') {
  1547. // const res = await getHistoryMapData(session_list[key].id);
  1548. // if (res.code / 1 === 200) {
  1549. // session_list[key].mindMap = res.data.mindmap;
  1550. // } else {
  1551. // session_list[key].mindMap = '';
  1552. // }
  1553. // }
  1554. // }
  1555. // 将列表数据进行转化
  1556. const list = formatDialogHistory(session_list, props.CHAT_AGENT_INFOMATION);
  1557. // 如果历史记录不为空
  1558. // 则需要展示对话内容
  1559. // 此时欢迎内容就不需要展示
  1560. if (list.length !== 0) {
  1561. show_welcome.value = false;
  1562. // 滚动到底部
  1563. scroll2ListBottom();
  1564. }
  1565. chat_log_list.value = list;
  1566. // 处理最后一项是否有思维导图或者网页,有的话展示右侧按钮
  1567. toShowRightBtn();
  1568. } else {
  1569. // 异常
  1570. // TODO:处理异常
  1571. show_welcome.value = false;
  1572. // handleChatRequestionException({
  1573. // message: res.msg
  1574. // });
  1575. // 使用消息提示,而非界面错误提示
  1576. handleErrorMessage('历史会话异常:' + res.msg);
  1577. }
  1578. })
  1579. .catch((err) => {
  1580. console.log('err-mfsjakfjsdkalfjs', err);
  1581. show_welcome.value = false;
  1582. // handleChatRequestionException({
  1583. // message: err.message
  1584. // });
  1585. handleErrorMessage('历史会话错误:' + err.message);
  1586. });
  1587. } else {
  1588. // 无历史记录:等待创建会话内容(用户开始输入、提交完成),列表区域显示欢迎界面
  1589. }
  1590. }
  1591. );
  1592. const getHistoryMapData = async (message_id) => {
  1593. const res = await getMapById(message_id);
  1594. return res;
  1595. };
  1596. onMounted(() => {
  1597. const agent_id = props.agent_id;
  1598. const session_id = props.session_id;
  1599. /**
  1600. * 如果选择智能体,则不显示欢迎页面
  1601. *
  1602. */
  1603. if (props.chat_history) {
  1604. show_welcome.value = false;
  1605. } else {
  1606. if (!agent_id && !session_id) {
  1607. show_welcome.value = true;
  1608. } else {
  1609. show_welcome.value = false;
  1610. }
  1611. }
  1612. // 组件挂载
  1613. if (session_id) {
  1614. // 有历史记录:获取历史记录内容进行展示
  1615. getHistoryLogs(session_id)
  1616. .then(async (res) => {
  1617. if (res.code / 1 === 200) {
  1618. const res_data = res.data;
  1619. // 成功
  1620. const session_list = res_data.message;
  1621. // for (let key in session_list) {
  1622. // if (session_list[key].role == 'assistant') {
  1623. // const res = await getHistoryMapData(session_list[key].id);
  1624. // if (res.code / 1 === 200) {
  1625. // session_list[key].mindMap = res.data.mindMap;
  1626. // } else {
  1627. // session_list[key].mindMap = '';
  1628. // }
  1629. // }
  1630. // }
  1631. // 将列表数据进行转化
  1632. const list = formatDialogHistory(session_list, props.CHAT_AGENT_INFOMATION);
  1633. // 如果历史记录不为空
  1634. // 则需要展示对话内容
  1635. // 此时欢迎内容就不需要展示
  1636. if (list.length !== 0) {
  1637. show_welcome.value = false;
  1638. // 滚动到底部
  1639. scroll2ListBottom();
  1640. }
  1641. chat_log_list.value = list;
  1642. // 处理最后一项是否有思维导图或者网页,有的话展示右侧按钮
  1643. toShowRightBtn();
  1644. } else {
  1645. // 异常
  1646. // TODO:处理异常
  1647. show_welcome.value = false;
  1648. // handleChatRequestionException({
  1649. // message: res.msg
  1650. // });
  1651. // 使用消息提示,而非界面错误提示
  1652. handleErrorMessage('历史会话异常:' + res.msg);
  1653. }
  1654. })
  1655. .catch((err) => {
  1656. console.log('err-mfsjakfjsdkalfjs', err);
  1657. show_welcome.value = false;
  1658. // handleChatRequestionException({
  1659. // message: err.message
  1660. // });
  1661. handleErrorMessage('历史会话错误:' + err.message);
  1662. });
  1663. } else {
  1664. // 无历史记录:等待创建会话内容(用户开始输入、提交完成),列表区域显示欢迎界面
  1665. }
  1666. });
  1667. // ---------------------------------------------------------
  1668. onUnmounted(() => {
  1669. // 组件卸载,关闭组件内的websocket连接
  1670. // closeWSConnect();
  1671. });
  1672. defineExpose({
  1673. toShowWelcome,
  1674. toNewChat
  1675. });
  1676. </script>
  1677. <style lang="css" scoped>
  1678. .conversation-chat-container {
  1679. position: relative;
  1680. display: flex;
  1681. justify-content: center;
  1682. margin-left: auto;
  1683. margin-right: auto;
  1684. height: calc(100% - 52px);
  1685. padding-top: 52px;
  1686. }
  1687. .conversation-chat-left {
  1688. width: 25%;
  1689. flex-shrink: 0;
  1690. }
  1691. .conversation-chat-right {
  1692. width: 25%;
  1693. flex-shrink: 0;
  1694. }
  1695. .conversation-chat-main {
  1696. flex-grow: 1;
  1697. width: 100%;
  1698. height: 100%;
  1699. /* max-width: 960px; */
  1700. margin-left: auto;
  1701. margin-right: auto;
  1702. display: flex;
  1703. /* flex-direction: column; */
  1704. flex-direction: row;
  1705. /* gap: 5%; */
  1706. /* background-color: #aaa; */
  1707. }
  1708. .conversation-chat-main-left {
  1709. /* flex-grow: 1; */
  1710. /* flex-basis: 10%; */
  1711. width: 10%;
  1712. flex-direction: column;
  1713. overflow: hidden;
  1714. height: 100%;
  1715. }
  1716. .conversation-chat-main-content {
  1717. /* flex-grow: 5; */
  1718. /* flex-basis: 50%; */
  1719. flex-direction: column;
  1720. overflow: hidden;
  1721. margin: 0 auto;
  1722. height: 100%;
  1723. }
  1724. .conversation-chat-main-right {
  1725. /* flex-grow: 4; */
  1726. /* flex-basis: 40%; */
  1727. flex-direction: column;
  1728. overflow: hidden;
  1729. height: 100%;
  1730. text-align: right;
  1731. margin-right: 10px;
  1732. }
  1733. .conversation-chat-expand {
  1734. text-align: right;
  1735. height: 100%;
  1736. width: 100%;
  1737. }
  1738. .conversation-chat-mind-map {
  1739. position: relative;
  1740. /* width: 100%; */
  1741. height: 330px;
  1742. padding: 5px;
  1743. border-radius: 12px;
  1744. background-color: var(--color-bg-1);
  1745. }
  1746. .conversation-chat-mind-map-spin {
  1747. position: absolute;
  1748. left: 0;
  1749. top: 0;
  1750. width: 100%;
  1751. height: 330px;
  1752. padding: 5px;
  1753. border-radius: 12px;
  1754. background-color: var(--color-bg-1);
  1755. display: flex;
  1756. align-items: center;
  1757. justify-content: center;
  1758. }
  1759. .mind_map_error {
  1760. position: absolute;
  1761. left: 0;
  1762. top: 0;
  1763. width: 100%;
  1764. height: 330px;
  1765. display: flex;
  1766. align-items: center;
  1767. justify-content: center;
  1768. background-color: var(--color-bg-1);
  1769. }
  1770. .exit-button {
  1771. position: absolute;
  1772. right: 10px;
  1773. top: 10px;
  1774. border: none;
  1775. cursor: pointer;
  1776. z-index: 9999;
  1777. }
  1778. .conversation-chat-mind-map-tools {
  1779. text-align: right;
  1780. }
  1781. .conversation-chat-web-page {
  1782. margin-top: 20px;
  1783. height: calc(100% - 380px);
  1784. padding: 10px;
  1785. border-radius: 12px;
  1786. background-color: var(--color-bg-1);
  1787. box-sizing: border-box;
  1788. }
  1789. .conversation-chat-content {
  1790. flex-grow: 1;
  1791. height: calc(100% - 60px);
  1792. display: flex;
  1793. flex-direction: column;
  1794. overflow: hidden;
  1795. padding-top: 8px;
  1796. }
  1797. .conversation-chat-main-container {
  1798. height: 80%;
  1799. /* flex-grow: 1; */
  1800. }
  1801. .conversation-chat-area {
  1802. flex-grow: 1;
  1803. overflow: auto;
  1804. /* max-width: 960px; */
  1805. }
  1806. .history-list-loading {
  1807. display: flex;
  1808. justify-content: space-between;
  1809. align-items: center;
  1810. min-height: 300px;
  1811. width: 100%;
  1812. }
  1813. .history-list-loading-container {
  1814. display: flex;
  1815. justify-content: center;
  1816. align-items: center;
  1817. }
  1818. .conversation-input-attached {
  1819. flex-shrink: 0;
  1820. width: 100%;
  1821. margin-left: auto;
  1822. margin-right: auto;
  1823. /* margin-top: 5px; */
  1824. margin-bottom: -10px;
  1825. padding: 5px;
  1826. border-radius: 10px;
  1827. display: flex;
  1828. }
  1829. .conversation-chat-input {
  1830. flex-shrink: 0;
  1831. width: 100%;
  1832. margin-left: auto;
  1833. margin-right: auto;
  1834. margin-top: 5px;
  1835. margin-bottom: 5px;
  1836. padding: 5px;
  1837. border-radius: 10px;
  1838. /* display: flex; */
  1839. }
  1840. /* .scroll-left {
  1841. width: 15%;
  1842. flex-shrink: 0;
  1843. }
  1844. .scroll-right {
  1845. width: 15%;
  1846. flex-shrink: 0;
  1847. } */
  1848. </style>