Jelajahi Sumber

feat: 流数据请求方法封装

sunxiao 1 Minggu lalu
induk
melakukan
5095fdfb62

+ 8 - 0
src/api/chat.js

@@ -9,4 +9,12 @@ export const chatApi = {
     method: 'GET',
     url: '/front/bigModel/home/recommendQAList/' + data
   }),
+
+  /**
+   * 获取sessionId
+   */
+  getChatSessionTag: () => http({
+    method: 'GET',
+    url: '/front/bigModel/chat/generateSessionId'
+  }),
 }

+ 40 - 1
src/components/chat/ChatAnswer.vue

@@ -1,12 +1,51 @@
 <script setup>
 import { ref } from 'vue';
+
+const props = defineProps({
+  id: {
+    type: [String, Number],
+    default: ''
+  },
+  content: {
+    type: String,
+    default: ''
+  },
+  loading: {
+    type: Boolean,
+    default: false
+  },
+  delayLoading: {
+    type: Boolean,
+    default: false
+  },
+  isSatisfied: {
+    type: Number,
+    default: 2
+  },
+  toggleVisibleIcons: {
+    type: Boolean,
+    default: true
+  },
+  loadingText: {
+    type: String,
+    default: '内容生成中...'
+  },
+  isVisibleStopBtn: {
+    type: Boolean,
+    default: false
+  },
+  isVisibleResetBtn: {
+    type: Boolean,
+    default: false
+  }
+})
 </script>
 
 <template>
   <view class="answer-container">
     <view class="answer-inner">
       <view class="markdown-wrap">
-        污水首先通过粗格栅去除较大的悬浮物和固体杂质,随后通过细格
+        {{ content }}
       </view>
       <view class="tools-wrap">
         <view class="btn-stream">

+ 16 - 1
src/components/chat/ChatAsk.vue

@@ -1,12 +1,27 @@
 <script setup>
 import { ref } from 'vue';
+
+defineProps({
+  content: {
+    type: String,
+    default: ''
+  },
+  sessionId: {
+    type: String,
+    default: ''
+  },
+  uploadFileList: {
+    type: Array,
+    default: []
+  }
+})
 </script>
 
 <template>
   <view class="ask-container">
     <view class="ask-inner">
       <view class="ask-content">
-        污水首先通过粗格栅去除较大的悬浮物和固体杂质,随后通过细格 
+        {{ content }}
       </view>
     </view>
   </view>

+ 24 - 4
src/components/chat/ChatInput.vue

@@ -1,5 +1,25 @@
 <script setup>
-const modelInpValue = defineModel();
+import { ref, unref } from 'vue';
+
+const emit = defineEmits(['on-submit']); 
+// const modelInpValue = defineModel();
+
+const inpVal = ref('');
+
+
+// 提交问题
+const onEmitSubmit = () => {
+  const val = unref(inpVal);
+  // if (!val) {
+  //   return uni.showToast({ title: '请输入您的问题或需求', duration: 3000, icon: 'none' });
+  // }
+
+  // if (val.length > 2000) {
+  //   return uni.showToast({ title: '问题限制2000个字以内', duration: 3000, icon: 'none' });
+  // }
+
+  emit('on-submit', { showVal: val,  question: val, selectedOption: {} });
+};
 </script>
 
 <template>
@@ -10,16 +30,16 @@ const modelInpValue = defineModel();
       </div>
       <view class="inp-inner">
         <textarea
-          v-model="modelInpValue"
+          v-model.trim="inpVal"
           auto-height
-          :maxlength="1000"
+          :maxlength="2000"
           class="chat-inp"
           placeholder="输入您的问题或需求"
           placeholder-style="color:#9A9A9A"
         >
         </textarea>
       </view>
-      <view class="send-btn">
+      <view class="send-btn" @click="onEmitSubmit">
         <TheSvgIcon class="icon" src="icon-send-plane" size="30"></TheSvgIcon>
       </view>
     </view>

+ 1 - 1
src/components/layout/BasePublicLayout.vue

@@ -112,7 +112,7 @@ onMounted(() => {
 
     .scroll-content {
       display: flex;
-      width: 100%;
+      width: 100vw;
       height: 100%;
     }
   }

+ 45 - 0
src/composables/useChat.js

@@ -0,0 +1,45 @@
+import { ref, unref } from 'vue';
+
+export const useChat = () => {
+  const chatDataSource = ref([]);
+
+  const createChat = chat => {
+    chatDataSource.value = [chat];
+  }
+
+  const addChat = chat => {
+    chatDataSource.value.push(chat);
+  }
+
+  const updateChat = chat => {
+    const length = unref(chatDataSource).length;
+    const index = length ? length - 1 : length;
+    chatDataSource.value[index] = chat;
+  }
+
+  const stopChat = () => {
+    const length = unref(chatDataSource).length;
+    const index = length ? length - 1 : length;
+    const lastItem = chatDataSource.value[index];
+    chatDataSource.value[index] = { ...lastItem, loading: false, delayLoading: false };
+  }
+
+  const clearChat = () => {
+    chatDataSource.value = [];
+  }
+
+  const updateById = params => {
+    const i = chatDataSource.value.findIndex(({ id }) => id === params.id);
+    chatDataSource.value[i] = { ...chatDataSource.value[i], ...params};
+  }
+
+  return {
+    chatDataSource,
+    createChat,
+    addChat,
+    updateChat,
+    stopChat,
+    clearChat,
+    updateById
+  }
+}

+ 76 - 10
src/pages/answer/index.vue

@@ -1,14 +1,23 @@
 <script setup>
-import { onMounted, ref } from 'vue';
+import { onMounted, ref, unref } from 'vue';
 import { useUserStore } from '@/stores/modules/userStore';
 import { useRecommend } from '@/composables/useRecommend';
+import { chatApi } from '@/api/chat';
+import { streamChatRequest } from '@/utils/streamRequest';
+import { useChat } from '@/composables/useChat';
+
+const ANSWER_ID_KEY = '@@id@@';
 
 const userStore = useUserStore();
 
+const { chatDataSource, addChat, updateChat, clearChat, updateById } = useChat();
+
 const showRight = ref(null);
 
 const { recommendList } = useRecommend({ type: 0 });
 
+const currenSessionId = ref('');
+
 const inpValue = ref('');
 
 // const jumpChatView = () => {
@@ -32,6 +41,57 @@ const closeDrawer = () => {
   showRight.value.close();
 }
 
+const onRegenerate = ({ showVal, question, realQuestion, tools, uploadFileList }) => {
+
+  // 参数相关先不组合 - 后续统一整理
+  // const sessionId = unref(currenSessionId);
+  
+  streamChatRequest({
+    data: {"sessionId":"1b6d485e6aef4ec9bed46c0d37d4e303","showVal":"今天天气","question":"今天天气","module":0,"modelType":0,"isStrong":0,"tools":null,"onlineSearch":false,"prompt":null},
+    onProgress: (responseText) => {
+      const [ answer ] = responseText.split(ANSWER_ID_KEY);
+      
+      updateChat({
+        sessionId: "1b6d485e6aef4ec9bed46c0d37d4e303",
+        showVal: showVal,
+        question,
+        answer,
+        loading: true,
+        delayLoading: false,
+        uploadFileList
+      })
+
+      // console.log( "chatDataSource.value", chatDataSource.value );
+    }
+  })
+}
+
+// 提交问答
+const handleSubmit = async ({ showVal, question, selectedOption, realQuestion = '', uploadFileList = [] }) => {
+  /**
+   * showVal:         input输入框的内容 - 展示使用
+   * question:        问题 - 用于传给大模型使用
+   * selectedOption: 智能体-智能差数,这个需要后续完善
+   * **/ 
+  const { data: sessionId } = await chatApi.getChatSessionTag();
+  currenSessionId.value = sessionId;
+
+  addChat({
+    sessionId: '1b6d485e6aef4ec9bed46c0d37d4e303',
+    showVal,
+    question,
+    realQuestion,
+    answer: '',
+    loading: true,
+    delayLoading: true,
+    uploadFileList
+  })
+
+  onRegenerate({ showVal, question, realQuestion, tools: selectedOption?.tools || null, uploadFileList });
+
+
+}
+
 onMounted(() => {
   // setInterval(item => {
   //   num.value = num.value + 1;
@@ -58,21 +118,29 @@ onMounted(() => {
             '期待与您一同规划和完成未来的工作',
             '有任何重点或需讨论的事项,随时告诉我'
           ]"
-          :card-content="recommendList">
+          :card-content="recommendList"
+        >
         </ChatWelcome>
-        <ChatTaskGroup></ChatTaskGroup>
+        <ChatTaskGroup ></ChatTaskGroup>
       </view>
 
       <view class="qa-container">
-        <view clas="qa-item">
-          <ChatAsk></ChatAsk>
-          <ChatAnswer></ChatAnswer>
+        <view class="qa-item" v-for="item, index in chatDataSource" :key="item.id">
+          <ChatAsk :content="item.showVal" :sessionId="item.sessionId" :uploadFileList="item.uploadFileList"></ChatAsk>
+          <ChatAnswer
+            :id="item.id"
+            :content="item.answer"
+            :loading="item.loading"
+            :delay-loading="item.delayLoading"
+            :isSatisfied="item.isSatisfied"
+            :isVisibleResetBtn="chatDataSource.length - 1 === index"
+          ></ChatAnswer>
         </view>
       </view>
     </template>
 
     <template #footer>
-      <ChatInput v-model="inpValue"></ChatInput>
+      <ChatInput v-model="inpValue" @on-submit="handleSubmit"></ChatInput>
     </template>
   </BasePublicLayout>
 
@@ -98,10 +166,8 @@ onMounted(() => {
 }
 
 .qa-container {
+  width: 100vw;
   padding-top: 64rpx;
 }
 
-// .qu-item {
-//   padding: ;
-// }
 </style>

+ 55 - 0
src/utils/streamRequest.js

@@ -0,0 +1,55 @@
+import { useUserStore } from "@/stores/modules/userStore";
+
+const userStore = useUserStore();
+const token = userStore.userInfo?.token;
+const baseURL = import.meta.env.VITE_APP_BASE_API;
+
+const decoder = new TextDecoder("utf-8");
+
+// 解码 UTF-8 字符串
+const decodeUTF8 = arrBuff => decoder.decode(new Uint8Array(arrBuff));
+
+// stream request - chat
+export const streamChatRequest = async ({ data, onProgress, messages, onComplete, onError, onAbort } ) => {
+
+  const url = baseURL + '/grpc/inferStreamRag'
+  const Authorization = "Bearer " + token;
+
+  let isStopped = false;
+  let typedResult = "";
+
+  const requestTask = uni.request({
+    url,
+    enableChunked: true,
+    method: "POST",
+    header: {
+      "Content-Type": "application/json",
+      Authorization,
+    },
+    data,
+    success: (res) => {
+      console.log("请求完成", res);
+    },
+    fail: (err) => {
+      onError(err);
+    },
+  });
+
+  let accumulatedText = "";
+  let fullMessage = "";
+
+  requestTask.onChunkReceived(async (res) => {
+    const uint8Array = new Uint8Array(res.data);
+    const chunkText = decodeUTF8([...uint8Array]);
+    accumulatedText += chunkText;
+    onProgress && onProgress(accumulatedText);
+  });
+
+  return () => {
+		console.log('中断请求...');
+		isStopped = true;
+
+		requestTask.abort();
+
+	};
+};