AnswerView.vue 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. <script setup>
  2. import { ref, unref, computed, onMounted, onUnmounted } from 'vue';
  3. import { useMessage } from 'naive-ui';
  4. import { useChatStore } from '@/stores/modules/chatStore';
  5. import { BaseButton, RecodeCardItem, TheSubMenu, TheChatView, ChatWelcome } from '@/components';
  6. import { ChatAsk, ChatAnswer, ChatAgentInput } from '@/components/Chat';
  7. import { chatApi } from '@/api/chat';
  8. import { useInfinite, useScroll, useChat, useRecommend } from '@/composables';
  9. const ANSWER_ID_KEY = '@@id@@';
  10. let controller = new AbortController();
  11. const chatStore = useChatStore();
  12. const { recordList, isFetching, onScrolltolower, onReset } = useInfinite('/front/bigModel/qa/pageList', { module: 0 });
  13. const { scrollRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll();
  14. const { chatDataSource, addChat, updateChat, clearChat, updateById } = useChat();
  15. const { recommendList } = useRecommend({ type: 0 });
  16. const message = useMessage();
  17. const switchActive = ref(false);
  18. const activeItem = ref({});
  19. const isLoading = ref(false);
  20. const inputRef = ref(null);
  21. const recordActive = ref(null);
  22. const currenSessionId = ref(null);
  23. const isExistInHistory = computed(() => (recordList.value.findIndex(({ sessionId: sId }) => sId === unref(currenSessionId)) === -1));
  24. // 新建对话
  25. const handleCreateDialog = async () => {
  26. message.destroyAll();
  27. inputRef.value.clearFileList();
  28. if (unref(isLoading)) {
  29. return message.warning('当前对话生成中');
  30. }
  31. if (!unref(chatDataSource).length) {
  32. return message.info('已切换最新会话');
  33. }
  34. inputRef.value.clearInpVal();
  35. currenSessionId.value = null;
  36. recordActive.value = null;
  37. clearChat();
  38. }
  39. // 查询对话详情
  40. const handleChatDetail = async ({ sessionId }) => {
  41. isLoading.value = false;
  42. recordActive.value = sessionId;
  43. controller.abort();
  44. inputRef.value.clearInpVal();
  45. const { data } = await chatApi.getAnswerHistoryDetail({ sessionId });
  46. chatDataSource.value = data.map(item => {
  47. const uploadFileList = []
  48. if ( item.question.includes('file:') ) {
  49. const fileInfo = item.question.split("||");
  50. const fileArr = fileInfo[0].split(":");
  51. const file = fileArr[1];
  52. const url = fileInfo[1];
  53. const suffix = file.substring( file.lastIndexOf('.') + 1 ).toUpperCase();
  54. const originSuffix = file.substring( file.lastIndexOf('.') );
  55. const name = file.substring(0, file.lastIndexOf('.'))
  56. uploadFileList.push({
  57. name,
  58. originSuffix,
  59. suffix,
  60. url
  61. })
  62. }
  63. return ({ ...item, loading: false, uploadFileList})
  64. });
  65. currenSessionId.value = sessionId;
  66. scrollToBottom();
  67. }
  68. const onRegenerate = async ({ showVal, question, realQuestion, tools, uploadFileList }) => {
  69. controller = new AbortController();
  70. const sessionId = unref(currenSessionId);
  71. let fileQuestionStr = '';
  72. if ( uploadFileList && uploadFileList.length ) {
  73. const [ fileItem ] = uploadFileList;
  74. fileQuestionStr = `file:${fileItem.name + fileItem.originSuffix}||${fileItem.url}||${question}`
  75. }
  76. const params = {
  77. data: {
  78. sessionId,
  79. showVal: showVal,
  80. question: realQuestion || fileQuestionStr || question,
  81. module: 0,
  82. modelType: Number(unref(switchActive)),
  83. isStrong: Number(unref(switchActive)),
  84. tools,
  85. prompt: null
  86. // TODO: 后续大概率需要删除
  87. // topP: 0.9,
  88. // temperature: 0.7
  89. },
  90. signal: controller.signal,
  91. onDownloadProgress: ({ event }) => {
  92. const xhr = event.target;
  93. const { responseText } = xhr;
  94. const [ answer ] = responseText.split(ANSWER_ID_KEY);
  95. updateChat({
  96. sessionId,
  97. showVal: showVal,
  98. question,
  99. answer,
  100. loading: true,
  101. delayLoading: false,
  102. uploadFileList
  103. })
  104. scrollToBottomIfAtBottom();
  105. }
  106. }
  107. try {
  108. const { data } = await chatApi.getChatStream(params);
  109. const [answer, id] = data.split(ANSWER_ID_KEY);
  110. updateChat({
  111. id,
  112. showVal: showVal,
  113. sessionId,
  114. question,
  115. answer,
  116. loading: false,
  117. delayLoading: false,
  118. uploadFileList
  119. })
  120. scrollToBottomIfAtBottom();
  121. }
  122. catch (error) {
  123. console.log("取消了请求 - catch", error);
  124. }
  125. finally {
  126. isLoading.value = false;
  127. onReset();
  128. }
  129. }
  130. // 提交问题
  131. const handleSubmit = async ({ showVal, question, selectedOption, realQuestion = '', uploadFileList = []}) => {
  132. // 用于模拟 - 内容生成前置等待状态
  133. if (unref(isExistInHistory)) {
  134. const { data: sessionId } = await chatApi.getChatSessionTag();
  135. currenSessionId.value = sessionId;
  136. }
  137. isLoading.value = true;
  138. addChat({
  139. sessionId: unref(currenSessionId),
  140. showVal,
  141. question,
  142. realQuestion,
  143. answer: '',
  144. loading: true,
  145. delayLoading: true,
  146. uploadFileList
  147. })
  148. scrollToBottom();
  149. setTimeout(() => onRegenerate({ showVal, question, realQuestion, tools: selectedOption?.tools || null, uploadFileList }), 2 * 1000);
  150. }
  151. // 处理推荐问题
  152. const handleWelcomeRecommend = ({ question, realQuestion }) => {
  153. handleSubmit({showVal: question, question, realQuestion});
  154. }
  155. // 删除历史对话
  156. const handeChatDelete = async (id) => {
  157. await chatApi.deleteHistory(id);
  158. onReset();
  159. clearChat();
  160. message.success('删除成功');
  161. }
  162. // 停止问题生成
  163. const onStopChatStream = async ({ sessionId }) => {
  164. await chatApi.getStopChatStream(sessionId);
  165. return message.warning('已停止对话生成');
  166. }
  167. // 重新生成问题
  168. const onChatResetStream = (item) => {
  169. const { question, uploadFileList, showVal } = item;
  170. console.log(item);
  171. handleSubmit({showVal, question, uploadFileList});
  172. }
  173. onMounted(() => {
  174. const question = chatStore.chatQuestion;
  175. if (Object.keys(question).length) {
  176. handleWelcomeRecommend(chatStore.chatQuestion);
  177. chatStore.clearChatQuestion();
  178. }
  179. })
  180. onUnmounted(() => {
  181. controller.abort();
  182. })
  183. </script>
  184. <template>
  185. <section class="flex items-start h-full">
  186. <TheSubMenu title="历史记录" @scrollToLower="onScrolltolower" :loading="isFetching">
  187. <template #top>
  188. <div class="create-btn px-[11px] pb-[22px]">
  189. <BaseButton @click="handleCreateDialog" icon-name="tool-add-circle">新建对话</BaseButton>
  190. </div>
  191. </template>
  192. <div class="pr-[4px] text-[#5e5e5e]">
  193. <RecodeCardItem v-for="item, index in recordList" :key="item.sessionId + index" :title="item.showVal"
  194. :time="item.createTime" :data-item="item"
  195. :class="{ 'recode-card-item_active': recordActive === item.sessionId }" @on-click="handleChatDetail"
  196. @on-delete="handeChatDelete" />
  197. </div>
  198. </TheSubMenu>
  199. <TheChatView ref="scrollRef">
  200. <ChatWelcome title="您好,我是LibraAI专家问答" card-title="您可以试着问我:" :sub-title="[
  201. '期待与您一同规划和完成未来的工作。有任何重点或需讨论的事项,随时告诉我'
  202. ]" :card-content="recommendList" v-if="!chatDataSource.length" @on-click="handleWelcomeRecommend" />
  203. <div class="conversation-item" v-if="chatDataSource.length">
  204. <template v-for="item, index in chatDataSource" :key="item.id">
  205. <ChatAsk :content="item.showVal" :sessionId="item.sessionId" :uploadFileList="item.uploadFileList"></ChatAsk>
  206. <ChatAnswer
  207. :id="item.id"
  208. :content="item.answer"
  209. :loading="item.loading"
  210. :delay-loading="item.delayLoading"
  211. :isSatisfied="item.isSatisfied"
  212. :isVisibleResetBtn="chatDataSource.length - 1 === index"
  213. isVisibleStopBtn
  214. @on-click-stop="onStopChatStream(item)"
  215. @on-click-icon="params => updateById(params)"
  216. @on-click-reset="onChatResetStream(item)"
  217. >
  218. </ChatAnswer>
  219. </template>
  220. </div>
  221. <template #footer>
  222. <ChatAgentInput
  223. :active-item="activeItem"
  224. ref="inputRef"
  225. v-model:loading="isLoading"
  226. v-model:switch="switchActive"
  227. @on-click="handleSubmit"
  228. @on-enter="handleSubmit"
  229. ></ChatAgentInput>
  230. </template>
  231. </TheChatView>
  232. </section>
  233. </template>