Ver Fonte

feat: 智能助手

sunxiao há 10 meses atrás
pai
commit
1ef7fb8b4e

+ 2 - 1
package.json

@@ -5,7 +5,8 @@
   "type": "module",
   "scripts": {
     "dev": "vite",
-    "build": "run-p type-check \"build-only {@}\" --",
+    "build": "vite build",
+    "build-lint": "run-p type-check \"build-only {@}\" --",
     "preview": "vite preview",
     "build-only": "vite build",
     "type-check": "vue-tsc --build --force"

+ 8 - 8
src/App.vue

@@ -20,14 +20,14 @@ const themeOverrides: GlobalThemeOverrides = {
 
   },
   Menu: {
-    // itemTextColor: primaryColor,
-    // itemTextColorActive: primaryColor,
-    // itemTextColorActiveHover: primaryColor,
-    // itemTextColorChildActiveHover: primaryColor,
-    // itemTextColorChildActive: primaryColor,
-    // itemColorActive: '#FCFDFE',
-    // itemColorActiveHover: '#FCFDFE',
-    // arrowColorChildActive: primaryColor
+    itemTextColor: primaryColor,
+    itemTextColorActive: primaryColor,
+    itemTextColorActiveHover: primaryColor,
+    itemTextColorChildActiveHover: primaryColor,
+    itemTextColorChildActive: primaryColor,
+    itemColorActive: '#FCFDFE',
+    itemColorActiveHover: '#FCFDFE',
+    arrowColorChildActive: primaryColor
   },
   Scrollbar: {
     width: '0px',

Diff do ficheiro suprimidas por serem muito extensas
+ 6 - 0
src/assets/svgs/tool/report.svg


+ 2 - 1
src/components/Chat/ChatInput.vue

@@ -62,7 +62,8 @@ const clearInpVal = () => {
 }
 
 defineExpose({
-  clearInpVal
+  clearInpVal,
+  inpVal,
 })
 
 </script>

+ 7 - 0
src/components/Chat/ChatSite.vue

@@ -0,0 +1,7 @@
+<template>
+
+
+  后期 = 变成问答大组件
+
+
+</template>

+ 2 - 1
src/components/Layout/TheChatView.vue

@@ -70,7 +70,8 @@ defineExpose({ targetScrollDom });
       </div>
       <main class="chat-main h-full m-auto flex flex-col justify-between" :style="{height: isFooter ? 'calc(100% - 212px)' : 'calc(100% - 100px)'}">
         <div class="chat-scroll" ref="targetScrollDom">
-          <div class="w-[800px] m-auto pb-[20px]">
+          <!-- pb-[20px] -->
+          <div class="w-[800px] m-auto ">
             <slot></slot>
           </div>
         </div>

+ 3 - 22
src/components/Layout/TheMenu.vue

@@ -70,32 +70,13 @@ const menuOptions = [
   },
   {
     label: () => renderLabel('智能助手'),
-    icon: renderIcon({ name: 'menu-user' }),
+    icon: renderIcon({ name: 'menu-help' }),
     key: '/helper'
   },
   {
     label: () => renderLabel('用户中心'),
-    icon: renderIcon({ name: 'menu-help' }),
-    key: '/',
-    children: [
-      {
-        label: '账号管理',
-        icon: renderChildrenIcon({ name: 'menu-analyse-order' }),
-        key: '/account1',
-      },
-      {
-        label: '意见反馈',
-        icon: renderChildrenIcon({ name: 'menu-analyse-order' }),
-        key: '/account2',
-      },
-      {
-        // renderLabel('关于我们', 'https://www.sequoialibra.com/')
-        label: '关于我们',
-        icon: renderChildrenIcon({ name: 'menu-analyse-order' }),
-        url: 'https://www.sequoialibra.com/',
-        key: '/account3',
-      },
-    ]
+    icon: renderIcon({ name: 'menu-user' }),
+    key: '/user',
   },
 ]
 

+ 6 - 5
src/components/RecodeSquareCardItem/index.vue

@@ -2,6 +2,7 @@
 import { ref, computed } from 'vue';
 import { NEllipsis } from "naive-ui";
 import { BaseButton, SvgIcon } from '@/components';
+import { truncateToTwoDecimalPlaces } from '@/utils/format';
 
 const props = defineProps({
   item: {
@@ -24,11 +25,11 @@ const warningType = computed(() => {
       cls: 'tips_success'
     },
     '2': {
-      label: '系统自动关闭',
-      cls: 'tips_success'
+      label: '系统关闭',
+      cls: 'tips_close'
     },
     '3': {
-      label: '用户转为应急处理中',
+      label: '应急处理中',
       cls: 'tips_warning'
     }
   }
@@ -39,7 +40,7 @@ const warningType = computed(() => {
     },
     '0': {
       label: '已完成',
-      cls: 'tips_warning'
+      cls: 'tips_success'
     },
   }
 
@@ -59,7 +60,7 @@ const dataSources = computed(() => {
   if (item.type == 0) {
     return [
       { label: '报警时间', value: item.time },
-      { label: '报警值',   value: item.warningVal, type: 'wraning' },
+      { label: '报警值',   value: truncateToTwoDecimalPlaces(item.warningVal), type: 'wraning' },
       { label: '报警级别', value: item.level },
       { label: '报警次数', value: item.counts },
     ]

+ 5 - 4
src/utils/format.js

@@ -1,7 +1,5 @@
 const formatTextData = (dataSource, whileList) => {
 
-
-
 }
 
 export const format = {
@@ -17,6 +15,9 @@ export const format = {
     })
 
     return { title, list };
-  },
+  }
+}
 
-}
+export const truncateToTwoDecimalPlaces = num => {
+  return (Math.floor(num * 100) / 100).toFixed(2);
+}

+ 1 - 1
src/utils/request.ts

@@ -71,7 +71,7 @@ export class Request {
       if ( res.config.onDownloadProgress ) return res;
    
       const { code } = res.data;
-      console.log(code)
+      // console.log(code)
       // !success && showNotification("error", message);
       return code === 200 ? res.data : Promise.reject(res.data);
 

+ 1 - 1
src/views/analyse/PymolView.vue

@@ -127,7 +127,7 @@ const handleWelcomeRecommend = question => {
 
 <template>
   <section class="flex items-start h-full" id="warning">
-    <TheSubMenu title="水质报警" @scrollToLower="onScrolltolower" :loading="isFetching">
+    <TheSubMenu title="生化报警" @scrollToLower="onScrolltolower" :loading="isFetching">
       <template #top>
         <div class="border-[#DAE5ED]">
           <n-tabs type="line" justify-content="space-evenly">

+ 5 - 5
src/views/analyse/WaterView.vue

@@ -6,7 +6,7 @@ import { useChatStore } from '@/stores/modules/chatStore';
 import { BaseTable, ChatWelcome, RecodeSquareCardItem, TheSubMenu, TheChatView } from "@/components";
 import { ChatBaseCard, ChatAnswer } from '@/components/Chat';
 
-import { format } from "@/utils/format";
+import { format, truncateToTwoDecimalPlaces } from "@/utils/format";
 
 import { waterApi } from '@/api/water';
 import { CustomModal } from "./components";
@@ -46,7 +46,7 @@ const visible = ref(false);
 const renderRowDom = ({ row, key }) => {
   const { exceed, value } = row[key] || {};
   const cls = exceed ? 'text-[#F44C49] font-bold' : 'text-[1A2029]'
-  return (<span class={ cls }>{value} {exceed && <i>↑</i>}</span>)
+  return (<span class={ cls }>{truncateToTwoDecimalPlaces(value)} {exceed && <i>↑</i>}</span>)
 } 
 
 const columns = [
@@ -87,7 +87,7 @@ const columns = [
     render: (row) => renderRowDom({ row, key: 'NH3-N' })
   },
   {
-    title: '总磷TP(mg/L)',
+    title: 'TP(mg/L)',
     key: 'COD',
     titleAlign: 'center',
     align: 'center',
@@ -360,7 +360,7 @@ const handleWelcomeRecommend = question => {
         </div>
       </ChatBaseCard>
 
-      {{  answerResult.length  }}
+      <!-- {{  answerResult.length  }} -->
 
       <section v-for="item,index in answerResult" :key="index">
         <template v-if="item.biz === 'DECISION_REPORT'">
@@ -371,7 +371,7 @@ const handleWelcomeRecommend = question => {
             :content="item.answer"
           ></ChatAnswer>
         </template>
-        {{ item }}
+        <!-- {{ item }} -->
         <template v-if="item.biz === 'DECISION_ALERT'">
           <ChatBaseCard
             :loading="item.loading"

+ 172 - 0
src/views/analyse/WorkOrder-old.vue

@@ -0,0 +1,172 @@
+<script setup>
+import { ref, unref, computed, onMounted } from 'vue';
+import { useRoute } from 'vue-router';
+import { useMessage, NDatePicker, NInput, NButton } from 'naive-ui'
+import { useChatStore } from '@/stores/modules/chatStore';
+import { BaseButton, RecodeCardItem, TheSubMenu, TheChatView, ChatWelcome, SvgIcon } from '@/components';
+import { ChatAsk, ChatAnswer, ChatInput } from '@/components/Chat';
+import { chatApi } from '@/api/chat';
+
+import { useInfinite } from '@/composables/useInfinite';
+import { useScroll } from '@/composables/useScroll';
+import { useChat } from '@/composables/useChat';
+import { useRecommend } from '@/composables/useRecommend';
+
+// TODO: 如果这里的key不一样,将会在拆一层组件出来 - list
+const { recordList, isFetching, onScrolltolower, onReset, addHistoryRecord } = useInfinite('/front/bigModel/qa/pageList', { module: 1 });
+const { scrollRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll();
+const { chatDataSource, addChat, updateChat, clearChat, updateById } = useChat();
+const { recommendList } = useRecommend({ type: 0 });
+
+
+const oneText = ref('');
+const twoText = ref('');
+
+</script>
+
+<template>
+  <section class="flex items-start h-full">
+
+    <TheSubMenu title="历史记录" @scrollToLower="onScrolltolower" :loading="isFetching">
+      <template #top>
+        <div class="create-btn px-[11px] pb-[22px]">
+          <BaseButton @click="handleCreateDialog">新建工单</BaseButton>
+        </div>
+      </template>
+
+      <div class="pr-[4px] text-[#5e5e5e]">
+        <RecodeCardItem v-for="item, index in recordList" :key="item.sessionId + index" :title="item.showVal"
+          :time="item.createTime" :data-item="item" @on-click="handleChatDetail" @on-delete="handeChatDelete" />
+      </div>
+    </TheSubMenu>
+
+    <TheChatView ref="scrollRef" :is-footer="false">
+      <ChatWelcome 
+        title="您好,我是LibraAI智慧工单助手" 
+        card-title="您可以试着问我:"
+        :sub-title="[
+          '基于大语言模型的智能数据分析助手,可以为您实现数据分析及数据解读',
+          '选择日期并填写关键信息为您生成日报工单'
+        ]"
+        @on-click="handleWelcomeRecommend"
+        v-if="1"
+      />
+
+      <div class="order-container px-[60px] py-[30px] mt-[36px] rounded-[10px] bg-[#fff]">
+        <div class="flex items-end justify-start space-x-[16px] pb-[20px] border-b-[1px] border-solid border-[#F1F1F1]">
+          <span class="text-[20px] leading-[28px] font-bold">智慧工单报告</span>
+          <span class="text-[12px] text-[#8F959C]">填写下面关键信息为您生成工单分析</span>
+        </div>
+
+        <main class="order pt-[20px] text-[#1A2029]">
+          <div class="flex items-center justify-start text-[16px] space-x-[16px]">
+            <span class="font-bold">选择时间</span>
+            <div class="w-[164px] border-[1px] border-[#EFEFF0] rounded-[8px] overflow-hidden">
+              <NDatePicker placeholder="选择日期" :readonly="true">
+                <template #date-icon>
+                  <SvgIcon name="tool-arrow-bottom"></SvgIcon>
+                </template>
+              </NDatePicker>
+            </div>
+          </div>
+
+          <div class="flex items-end justify-start my-[20px] text-[16px] space-x-[16px]">
+            <span class="font-bold leading-[22px]">报告内容</span>
+            <span class="text-[12px] text-[#8F959C] leading-[16px]">可直接点击“立即生成”输出报告,也可以在输入框内调整您要分析的内容</span>
+          </div>
+
+          <ul class="flex flex-col text-[14px]">
+            <li class="list-item"><span class="title">1、水质数据、生化数据情况</span></li>
+            <li class="list-item">
+              <p class="item-top">
+                <span class="title">2、指标数据的分析</span>
+                <span class="num">{{ oneText.length }}/100</span>
+              </p>
+               <n-input
+                class="text-[14px] border-[1px] border-[#EFEFF0] bg-[#f8f8fa] rounded-[8px]"
+                clearable
+                maxlength="100"
+                placeholder="请输入您的建议,100字内"
+                type="textarea" 
+                size="large" 
+                :autosize="{ minRows: 1, maxRows: 2 }"
+                v-model:value="oneText" 
+              />
+            </li>
+            <li class="list-item">
+              <p class="item-top">
+                <span class="title">3、根据分析法,输出分析报告</span>
+                <span class="num">{{ twoText.length }}/100</span>
+              </p>
+              <n-input
+                class="text-[14px] border-[1px] border-[#EFEFF0] bg-[#f8f8fa] rounded-[8px]"
+                clearable
+                maxlength="100"
+                placeholder="请输入您的建议,100字内"
+                type="textarea" 
+                size="large"
+                :autosize="{ minRows: 1, maxRows: 2 }"
+                v-model:value="oneText"
+              />
+            </li>
+          </ul>
+        </main>
+
+        <footer class="pt-[8px]">
+          <button class="btn-primary" @click="">立即生成</button>
+        </footer>
+      </div>
+    </TheChatView>
+  </section>
+</template>
+
+<style scoped lang="scss">
+.n-input__input-el {
+  font-size: 12px !important;
+}
+
+.list-item {
+  margin-bottom: 16px;
+
+  .title {
+    font-size: 14px;
+    font-weight: bold;
+    line-height: 20px;
+  }
+
+  .item-top {
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 10px;
+
+    .num {
+      font-size: 12px;
+      font-weight: 400;
+      color: #B0B7C0;
+    }
+  }
+}
+
+
+.btn-primary {
+  width: 88px;
+  height: 32px;
+  border-radius: 6px;
+  background-color: #2454FF;
+  font-size: 14px;
+  line-height: 32px;
+  color: #fff;
+
+  &:hover {
+    background: #1D43CC;
+  }
+}
+</style>
+
+<style lang="scss">
+.order {
+  .n-input__input-el, .n-input__placeholder {
+    font-size: 14px;
+  }
+}
+</style>

+ 177 - 62
src/views/analyse/WorkOrder.vue

@@ -1,33 +1,169 @@
 <script setup>
-import { ref, unref, computed, onMounted } from 'vue';
-import { useRoute } from 'vue-router';
-import { useMessage, NDatePicker, NInput, NButton } from 'naive-ui'
-import { useChatStore } from '@/stores/modules/chatStore';
+import { ref, unref, computed, onUnmounted } from 'vue';
+import { useMessage, NDatePicker } from 'naive-ui';
 import { BaseButton, RecodeCardItem, TheSubMenu, TheChatView, ChatWelcome, SvgIcon } from '@/components';
-import { ChatAsk, ChatAnswer, ChatInput } from '@/components/Chat';
+import { ChatAsk, ChatAnswer } from '@/components/Chat';
 import { chatApi } from '@/api/chat';
 
-import { useInfinite } from '@/composables/useInfinite';
-import { useScroll } from '@/composables/useScroll';
-import { useChat } from '@/composables/useChat';
-import { useRecommend } from '@/composables/useRecommend';
+import {useInfinite, useScroll, useChat, useRecommend} from '@/composables';
 
-// TODO: 如果这里的key不一样,将会在拆一层组件出来 - list
-const { recordList, isFetching, onScrolltolower, onReset, addHistoryRecord } = useInfinite('/front/bigModel/qa/pageList', { model: 1 });
+const ANSWER_ID_KEY = '@@id@@';
+
+let controller = new AbortController();
+
+const { recordList, isFetching, onScrolltolower, onReset, addHistoryRecord } = useInfinite('/front/bigModel/qa/pageList', { module: 1 });
 const { scrollRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll();
 const { chatDataSource, addChat, updateChat, clearChat, updateById } = useChat();
-const { recommendList } = useRecommend({ type: 0 });
 
+const message = useMessage();
+const reportDate = ref();
+const switchActive = ref(false);
+
+const isLoading = ref(false);
+const inputRef = ref(null);
+
+const currenSessionId = ref(null);
+
+const isExistInHistory = computed(() => (recordList.value.findIndex(({ sessionId: sId }) => sId === unref(currenSessionId)) === -1));
+
+// 新建对话
+const handleCreateDialog = async () => {
+  message.destroyAll();
 
-const oneText = ref('');
-const twoText = ref('');
+  if (unref(isLoading)) {
+    return message.warning('当前对话生成中');
+  }
+
+  if (!unref(chatDataSource).length) {
+    return message.info('已切换最新会话');
+  }
+
+  currenSessionId.value = null;
+
+  clearChat();
+}
 
+// 查询对话详情
+const handleChatDetail = async ({ sessionId }) => {
+  isLoading.value = false;
+
+  controller.abort();
+
+  const { data } = await chatApi.getAnswerHistoryDetail({ sessionId });
+
+  chatDataSource.value = data.map(item => ({ ...item, loading: false,  }));
+  currenSessionId.value = sessionId;
+
+  scrollToBottom();
+}
+
+const onRegenerate = async ({ question, realQuestion }) => {
+  controller = new AbortController();
+
+  const sessionId = unref(currenSessionId);
+  const params = {
+    data: {
+      sessionId,
+      showVal: question,
+      question: realQuestion || question,
+      module: 1,
+      isStrong: Number(unref(switchActive)),
+      reportDate: reportDate.value
+    },
+    signal: controller.signal,
+    onDownloadProgress: ({ event }) => {
+      const xhr = event.target;
+      const { responseText } = xhr;
+      const [ answer ] = responseText.split(ANSWER_ID_KEY);
+
+      updateChat({
+        sessionId,
+        showVal:question,
+        answer,
+        loading: true,
+        delayLoading: false,
+      })
+
+      scrollToBottomIfAtBottom();
+    }
+  }
+
+  try {
+    const { data } = await chatApi.getChatStream(params);
+ 
+    const [ answer, id ] = data.split(ANSWER_ID_KEY);
+
+    updateChat({
+      id,
+      sessionId,
+      showVal: question,
+      answer,
+      loading: false,
+      delayLoading: false
+    })
+    
+    scrollToBottomIfAtBottom();
+  }
+  catch (error){
+    console.log("取消了请求 - catch", error);
+  }
+  finally {
+    isLoading.value = false;
+    onReset();
+  }
+}
+// 提交问题
+const handleSubmit = async (question, realQuestion = '') => {
+  // 用于模拟 - 内容生成前置等待状态
+  
+  if (unref(isExistInHistory)) {
+    const { data: sessionId } = await chatApi.getChatSessionTag();
+    currenSessionId.value = sessionId;
+  }
+  
+  isLoading.value = true;
+
+  addChat({
+    sessionId: unref(currenSessionId),
+    showVal: question,
+    realQuestion,
+    answer: '',
+    loading: true,
+    delayLoading: true,
+  })
+
+  scrollToBottom();
+
+  setTimeout(() => onRegenerate({ question, realQuestion }), 2 * 1000);
+}
+
+// 处理推荐问题
+const handleCreateOrder = () => {
+  
+  if ( !reportDate.value ) {
+    return message.warning('请选择日期');
+  }
+
+  handleSubmit(`请生成${reportDate.value}智能工单分析报告`);
+}
+
+// 删除历史对话
+const handeChatDelete = async (id) => {
+  await chatApi.deleteHistory(id);
+  onReset();
+  clearChat();
+  message.success('删除成功');
+}
+
+onUnmounted(() => {
+  controller.abort();
+})
 </script>
 
 <template>
   <section class="flex items-start h-full">
 
-    <TheSubMenu title="历史记录" @scrollToLower="onScrolltolower" :loading="isFetching">
+    <TheSubMenu title="智能工单" @scrollToLower="onScrolltolower" :loading="isFetching">
       <template #top>
         <div class="create-btn px-[11px] pb-[22px]">
           <BaseButton @click="handleCreateDialog">新建工单</BaseButton>
@@ -48,21 +184,34 @@ const twoText = ref('');
           '基于大语言模型的智能数据分析助手,可以为您实现数据分析及数据解读',
           '选择日期并填写关键信息为您生成日报工单'
         ]"
-        @on-click="handleWelcomeRecommend"
-        v-if="1"
+        v-if="!chatDataSource.length"
       />
 
-      <div class="order-container px-[60px] py-[30px] mt-[36px] rounded-[10px] bg-[#fff]">
+      <div class="conversation-item" v-if="chatDataSource.length">
+        <template v-for="item in chatDataSource" :key="item.id">
+          <ChatAsk :content="item.showVal" :sessionId="item.sessionId"></ChatAsk>
+          <ChatAnswer
+            :id="item.id"
+            :content="item.answer"
+            :loading="item.loading"
+            :delay-loading="item.delayLoading"
+            :isSatisfied="item.isSatisfied"
+            @on-click-icon=" params => updateById(params)"
+          ></ChatAnswer>
+        </template>
+      </div>
+
+      <div class="order-container px-[60px] py-[30px] mt-[36px] rounded-[10px] bg-[#fff]" v-if="!chatDataSource.length">
         <div class="flex items-end justify-start space-x-[16px] pb-[20px] border-b-[1px] border-solid border-[#F1F1F1]">
           <span class="text-[20px] leading-[28px] font-bold">智慧工单报告</span>
-          <span class="text-[12px] text-[#8F959C]">填写下面关键信息为您生成工单分析</span>
+          <span class="text-[12px] text-[#8F959C]">选择日期后为您生成日报工单</span>
         </div>
 
         <main class="order pt-[20px] text-[#1A2029]">
           <div class="flex items-center justify-start text-[16px] space-x-[16px]">
             <span class="font-bold">选择时间</span>
             <div class="w-[164px] border-[1px] border-[#EFEFF0] rounded-[8px] overflow-hidden">
-              <NDatePicker placeholder="选择日期" :readonly="true">
+              <NDatePicker placeholder="选择日期" :readonly="true" v-model:formatted-value="reportDate" value-format="yyyy-MM-dd">
                 <template #date-icon>
                   <SvgIcon name="tool-arrow-bottom"></SvgIcon>
                 </template>
@@ -70,50 +219,16 @@ const twoText = ref('');
             </div>
           </div>
 
-          <div class="flex items-end justify-start my-[20px] text-[16px] space-x-[16px]">
-            <span class="font-bold leading-[22px]">报告内容</span>
-            <span class="text-[12px] text-[#8F959C] leading-[16px]">可直接点击“立即生成”输出报告,也可以在输入框内调整您要分析的内容</span>
-          </div>
-
-          <ul class="flex flex-col text-[14px]">
-            <li class="list-item"><span class="title">1、水质数据、生化数据情况</span></li>
-            <li class="list-item">
-              <p class="item-top">
-                <span class="title">2、指标数据的分析</span>
-                <span class="num">{{ oneText.length }}/100</span>
-              </p>
-              <n-input
-                class="text-[14px] border-[1px] border-[#EFEFF0] bg-[#f8f8fa] rounded-[8px]"
-                clearable
-                maxlength="100"
-                placeholder="请输入您的建议,100字内"
-                type="textarea" 
-                size="large" 
-                :autosize="{ minRows: 1, maxRows: 2 }"
-                v-model:value="oneText"
-              />
-            </li>
-            <li class="list-item">
-              <p class="item-top">
-                <span class="title">3、根据分析法,输出分析报告</span>
-                <span class="num">{{ twoText.length }}/100</span>
-              </p>
-              <n-input
-                class="text-[14px] border-[1px] border-[#EFEFF0] bg-[#f8f8fa] rounded-[8px]"
-                clearable
-                maxlength="100"
-                placeholder="请输入您的建议,100字内"
-                type="textarea" 
-                size="large"
-                :autosize="{ minRows: 1, maxRows: 2 }"
-                v-model:value="oneText"
-              />
-            </li>
-          </ul>
+          <dl class="pt-[26px] text-[#1A2029] space-y-[16px]">
+            <dt class="text-[16px] font-bold leading-[22px]">报告内容</dt>
+            <dd>1、水质数据、生化数据情况</dd>
+            <dd>2、各项指标数据分析</dd>
+            <dd>3、对于工艺调整方面、数据趋势方面的措施与建议</dd>
+          </dl>
         </main>
 
-        <footer class="pt-[8px]">
-          <button class="btn-primary" @click="">立即生成</button>
+        <footer class="pt-[24px]">
+          <button class="btn-primary" @click="handleCreateOrder">立即生成</button>
         </footer>
       </div>
     </TheChatView>

+ 5 - 8
src/views/answer/AnswerView.vue

@@ -1,26 +1,20 @@
 <script setup>
 import { ref, unref, computed, onMounted, onUnmounted } from 'vue';
-import { useRoute } from 'vue-router';
 import { useMessage } from 'naive-ui';
 import { useChatStore } from '@/stores/modules/chatStore';
 import { BaseButton, RecodeCardItem, TheSubMenu, TheChatView, ChatWelcome } from '@/components';
 import { ChatAsk, ChatAnswer, ChatInput } from '@/components/Chat';
 import { chatApi } from '@/api/chat';
 
-import { useInfinite } from '@/composables/useInfinite';
-import { useScroll } from '@/composables/useScroll';
-import { useChat } from '@/composables/useChat';
-import { useRecommend } from '@/composables/useRecommend';
+import {useInfinite, useScroll, useChat, useRecommend} from '@/composables';
 
 const ANSWER_ID_KEY = '@@id@@';
 
 let controller = new AbortController();
 
-const route = useRoute();
 const chatStore = useChatStore();
 
-// TODO: 如果这里的key不一样,将会在拆一层组件出来 - list
-const { recordList, isFetching, onScrolltolower, onReset, addHistoryRecord } = useInfinite('/front/bigModel/qa/pageList', { model: 0 });
+const { recordList, isFetching, onScrolltolower, onReset } = useInfinite('/front/bigModel/qa/pageList', { module: 0 });
 const { scrollRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll();
 const { chatDataSource, addChat, updateChat, clearChat, updateById } = useChat();
 const { recommendList } = useRecommend({type: 0});
@@ -112,6 +106,8 @@ const onRegenerate = async ({ question, realQuestion }) => {
       loading: false,
       delayLoading: false
     })
+    
+    scrollToBottomIfAtBottom();
   }
   catch (error){
     console.log("取消了请求 - catch", error);
@@ -204,6 +200,7 @@ onUnmounted(() => {
         v-if="!chatDataSource.length"
         @on-click="handleWelcomeRecommend"
       />
+      
       <div class="conversation-item" v-if="chatDataSource.length">
         <template v-for="item in chatDataSource" :key="item.id">
           <ChatAsk :content="item.question" :sessionId="item.sessionId"></ChatAsk>

+ 190 - 113
src/views/helper/HelperView.vue

@@ -1,107 +1,166 @@
 <script setup>
 import { ref, unref, computed, onMounted, onUnmounted } from 'vue';
-import { useRoute } from 'vue-router';
 import { useMessage } from 'naive-ui';
-import { useChatStore } from '@/stores/modules/chatStore';
-import { BaseButton, RecodeCardItem, TheSubMenu, TheChatView, ChatWelcome } from '@/components';
+import { BaseButton, RecodeCardItem, TheSubMenu, TheChatView, ChatWelcome, SvgIcon } from '@/components';
 import { ChatAsk, ChatAnswer, ChatInput } from '@/components/Chat';
+import { chatApi } from '@/api/chat';
 import { helperApi } from '@/api/helper';
 
+import { useInfinite, useScroll, useChat } from '@/composables';
 
-import { useInfinite } from '@/composables/useInfinite';
-import { useScroll } from '@/composables/useScroll';
-import { useChat } from '@/composables/useChat';
-import { useRecommend } from '@/composables/useRecommend';
+const ANSWER_ID_KEY = '@@id@@';
 
+let controller = new AbortController();
+
+// TODO: 如果这里的key不一样,将会在拆一层组件出来 - list
 const { recordList, isFetching, onScrolltolower, onReset, addHistoryRecord } = useInfinite('/front/bigModel/qa/pageList', { module: 2 });
+const { scrollRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll();
+const { chatDataSource, addChat, updateChat, clearChat, updateById } = useChat();
 
-const dataList = ref(new Array(100).fill({ a:"hello" }));
-const isLoading = ref(true);
-
-const lists = [
-	{
-		title:'报告研究',
-		content:'写出一篇有关 #智慧水务# 的500字研究报告,报告中需引述最新的研究,并引用专家观点',
-	},
-	{
-		title:'报告开场白',
-		content:'我现在正在写一篇大学生研究报告,报告的开场白需要让普通人能快速理解。我的报告主题是 #关于AIGC快速发展的调查和研究#,请提供三种开头方式,要简单到普通人都能听懂,同时要足够能吸引人,让他们愿意专心听下去',
-	},
-	{
-		title:'邮件回复',
-		content:'你扮演一个 #调度工程师#,帮我写一封邮件,邮件的内容是 #向公司的领导申请更多的开发资源支持#,邮件内容需要简单明了,语气大方得体,使用中文表达'
-	},{
-		title:'周报撰写',
-		content:'你扮演一个资深员工,我把这一周的工作内容发给你,你帮我撰写为一篇完整的周报。周报要分成本周工作总结、下周工作计划、遇到的困难和解决方案、个人感悟四个段落分别撰写。每一段落都要以分条列表形式写出具体事项,你要一步一步的引导我,让我写出一篇完整的周报。'
-	},{
-		title:'PPT大纲',
-		content:'你扮演一个PPT专家,我把PPT主题#论智慧水务新时代#发给你,请自动生成主题、内容、讲稿'
-	},{
-		title:'报告开场白',
-		content:'我现在正在写一篇大学生研究报告,报告的开场白需要让普通人能快速理解。我的报告主题是 #关于AIGC快速发展的调查和研究#,请提供三种开头方式,要简单到普通人都能听懂,同时要足够能吸引人,让他们愿意专心听下去',
-	},{
-		title:'SWTO分析',
-		content:'你现在扮演专业的分析研究员,帮我针对#智慧水务# 做一个SWOT分析,要求尽可能详细'
-	},{
-		title:'周报撰写',
-		content:'你扮演一个资深员工,我把这一周的工作内容发给你,你帮我撰写为一篇完整的周报。周报要分成本周工作总结、下周工作计划、遇到的困难和解决方案、个人感悟四个段落分别撰写。每一段落都要以分条列表形式写出具体事项,你要一步一步的引导我,让我写出一篇完整的周报。'
-	},{
-		title:'PPT大纲',
-		content:'你扮演一个PPT专家,我把PPT主题#论智慧水务新时代#发给你,请自动生成主题、内容、讲稿'
-	},{
-		title:'SWTO分析',
-		content:'你现在扮演专业的分析研究员,帮我针对#智慧水务# 做一个SWOT分析,要求尽可能详细'
-	},{
-		title:'SWTO分析',
-		content:'你现在扮演专业的分析研究员,帮我针对#智慧水务# 做一个SWOT分析,要求尽可能详细'
-	},{
-		title:'SWTO分析',
-		content:'你现在扮演专业的分析研究员,帮我针对#智慧水务# 做一个SWOT分析,要求尽可能详细'
-	},
-	{
-		title:'报告研究',
-		content:'写出一篇有关 #智慧水务# 的500字研究报告,报告中需引述最新的研究,并引用专家观点',
-	},
-	{
-		title:'报告研究',
-		content:'写出一篇有关 #智慧水务# 的500字研究报告,报告中需引述最新的研究,并引用专家观点',
-	},
-	{
-		title:'报告研究',
-		content:'写出一篇有关 #智慧水务# 的500字研究报告,报告中需引述最新的研究,并引用专家观点',
-	},
-	{
-		title:'报告研究',
-		content:'写出一篇有关 #智慧水务# 的500字研究报告,报告中需引述最新的研究,并引用专家观点',
-	},
-	{
-		title:'报告研究',
-		content:'写出一篇有关 #智慧水务# 的500字研究报告,报告中需引述最新的研究,并引用专家观点',
-	},
-	{
-		title:'报告研究',
-		content:'写出一篇有关 #智慧水务# 的500字研究报告,报告中需引述最新的研究,并引用专家观点',
-	},
-]
-
-const getMoreData = () => {
-  
+const helperList = ref([]);
+const message = useMessage();
+
+const switchActive = ref(false);
+
+const isLoading = ref(false);
+const inputRef = ref(null);
+
+const currenSessionId = ref(null);
+
+const isExistInHistory = computed(() => (recordList.value.findIndex(({ sessionId: sId }) => sId === unref(currenSessionId)) === -1));
+
+// 新建对话
+const handleCreateDialog = async () => {
+  message.destroyAll();
+
+  if (unref(isLoading)) {
+    return message.warning('当前对话生成中');
+  }
+
+  if (!unref(chatDataSource).length) {
+    return message.info('已切换最新会话');
+  }
+
+  inputRef.value.clearInpVal();
+
+  currenSessionId.value = null;
+
+  clearChat();
 }
 
+// 查询对话详情
+const handleChatDetail = async ({ sessionId }) => {
+  isLoading.value = false;
+
+  controller.abort();
 
-const fetchHelperList = () => {
+  const { data } = await chatApi.getAnswerHistoryDetail({ sessionId });
 
+  chatDataSource.value = data.map(item => ({ ...item, loading: false,  }));
+  currenSessionId.value = sessionId;
+
+  scrollToBottom();
 }
 
-const handleCreateDialog = () => {
+const onRegenerate = async ({ question, realQuestion }) => {
+  controller = new AbortController();
+
+  const sessionId = unref(currenSessionId);
+  const params = {
+    data: {
+      sessionId,
+      showVal: question,
+      question: realQuestion || question,
+      module: 2,
+      isStrong: Number(unref(switchActive))
+    },
+    signal: controller.signal,
+    onDownloadProgress: ({ event }) => {
+      const xhr = event.target;
+      const { responseText } = xhr;
+      const [ answer ] = responseText.split(ANSWER_ID_KEY);
+
+      updateChat({
+        sessionId,
+        question,
+        answer,
+        loading: true,
+        delayLoading: false
+      })
 
+      scrollToBottomIfAtBottom();
+    }
+  }
+
+  try {
+    const { data  } = await chatApi.getChatStream(params);
+ 
+    const [ answer, id ] = data.split(ANSWER_ID_KEY);
+
+    updateChat({
+      id,
+      sessionId,
+      question,
+      answer,
+      loading: false,
+      delayLoading: false
+    })
+    
+    scrollToBottomIfAtBottom();
+  }
+  catch (error){
+    console.log("取消了请求 - catch", error);
+  }
+  finally {
+    isLoading.value = false;
+    onReset();
+  }
 }
+// 提交问题
+const handleSubmit = async (question, realQuestion = '') => {
+  // 用于模拟 - 内容生成前置等待状态
+  
+  if (unref(isExistInHistory)) {
+    const { data: sessionId } = await chatApi.getChatSessionTag();
+    currenSessionId.value = sessionId;
+  }
+  
+  isLoading.value = true;
+
+  addChat({
+    sessionId: unref(currenSessionId),
+    question,
+    realQuestion,
+    answer: '',
+    loading: true,
+    delayLoading: true
+  })
 
+  scrollToBottom();
 
-onMounted(() => {
+  setTimeout(() => onRegenerate({ question, realQuestion }), 2 * 1000);
+}
+
+// 处理推荐问题
+const handleWelcomeRecommend = ({ content }) => {
+  inputRef.value.inpVal = content;
+}
+
+// 删除历史对话
+const handeChatDelete = async (id) => {
+  await chatApi.deleteHistory(id);
+  onReset();
+  clearChat();
+  message.success('删除成功');
+}
 
-  helperApi.getHelperList();
+onMounted(async () => {
+  const { data } = await helperApi.getHelperList();
+  helperList.value = data;
+})
 
+onUnmounted(() => {
+  controller.abort();
 })
 </script>
 <template>
@@ -109,7 +168,7 @@ onMounted(() => {
     <TheSubMenu title="历史记录" @scrollToLower="onScrolltolower" :loading="isFetching">
       <template #top>
         <div class="create-btn px-[11px] pb-[22px]">
-          <BaseButton @click="handleCreateDialog">新建对话</BaseButton>
+          <BaseButton @click="handleCreateDialog">新建指令</BaseButton>
         </div>
       </template>
 
@@ -126,29 +185,31 @@ onMounted(() => {
       </div>
     </TheSubMenu>
 
-    
-
     <TheChatView ref="scrollRef">
-      <ChatWelcome title="您好,我是LibraAI专家问答" card-title="您可以试着问我:"
+      <div v-if="!chatDataSource.length">
+        <ChatWelcome title="您好,我是LibraAI专家问答" card-title="您可以试着问我:"
         :sub-title="[
           'LibarAI智能助手模块,具有强大的文本理解和生成能力,可快速响应用户需求',
           '提供撰写文章、生成报告等服务。'
-        ]" 
-        :card-content="recommendList"
-        @on-click="handleWelcomeRecommend"
-      />
-
+        ]"
+        />
         <div class="grid-container">
           <div class="grid-content">
-            <div class="grid-item" v-for="item,index in lists">
-              <div class="grid-item-icon">
-                <SvgIcon name="report"></SvgIcon>
-                <h3 class="grid-item-title">{{index}}{{item.title}}</h3>
+            <div
+              class="grid-item"
+              v-for="item in helperList"
+              :key="item.id"
+              @click="handleWelcomeRecommend(item)"
+            >
+              <div class="grid-item-icon space-x-[8px]">
+                <SvgIcon name="tool-report" size="24"></SvgIcon>
+                <h3 class="grid-item-title">{{item.title}}</h3>
               </div>
-              <div class="text-[#555555] mt-2">
-                <!--如果item.content中包含#号,把#号用span标签包裹并设置为蓝色--->
+              <div class="text-[#5E5E5E] mt-[8px] text-justify">
                 <template v-if="item.content.indexOf('#') !== -1">
-                  <span v-for="word in item.content.split('#')">{{word}}<span style="color: #2454FF;"> # </span></span>
+                  <span v-for="word, i in item.content.split('#')" :key="word">
+                    {{ word }}<i v-if="i !== item.content.split('#').length - 1" class="text-[#2454FF]">#</i>
+                  </span>
                 </template>
                 <template v-else>
                   <p>{{item.content}}</p>
@@ -157,6 +218,21 @@ onMounted(() => {
             </div>
           </div>
         </div>
+      </div>
+      
+      <div class="conversation-item" v-if="chatDataSource.length">
+        <template v-for="item in chatDataSource" :key="item.id">
+          <ChatAsk :content="item.question" :sessionId="item.sessionId"></ChatAsk>
+          <ChatAnswer
+            :id="item.id"
+            :content="item.answer"
+            :loading="item.loading"
+            :delay-loading="item.delayLoading"
+            :isSatisfied="item.isSatisfied"
+            @on-click-icon=" params => updateById(params)"
+          ></ChatAnswer>
+        </template>
+      </div>
 
       <template #footer>
         <ChatInput
@@ -174,7 +250,7 @@ onMounted(() => {
 <style scoped lang="scss">
 .grid-container{
 	position: relative;
-	height: calc(100% - 184px);
+	height: calc(100vh - 460px);
   padding-bottom: 20px;
   margin-top: 36px;
 	overflow: hidden;
@@ -185,23 +261,28 @@ onMounted(() => {
   }
 
 	.grid-content{
-		column-count:3; 
-		column-gap:20px;
-		-moz-column-count:3;
-		-webkit-column-count:3;
-		-moz-column-gap:20px;
-		-webkit-column-gap:20px;
+		column-count: 3; 
+		column-gap: 16px;
+		-moz-column-count: 3;
+		-webkit-column-count: 3;
+		-moz-column-gap: 16px;
+		-webkit-column-gap: 16px;
 
 		.grid-item{
 			height: auto;
-			padding: 20px 16px 16px 16px;
-			margin-bottom: 20px;
+			padding: 16px;
+			margin-bottom: 16px;
 			border-radius: 10px;
 			box-shadow: 0 2px 4px rgba(0,0,0,0.08);
 			background-color: #fff;
-			-moz-page-break-inside: avoid;
 			-webkit-column-break-inside: avoid;
 			break-inside: avoid;
+      border:1px solid #fff;
+      cursor: pointer;
+
+      &:hover {
+        border:1px solid #2454FF;
+      }
 
 			.grid-item-icon{
 				display: flex;
@@ -209,17 +290,13 @@ onMounted(() => {
 				justify-content: start;
 
 				.grid-item-title{
-					margin-left: 20px;
-					font-size: 16px;
+					font-size: 14px;
 					font-weight: 600;
 					color: #1A2029;
 				}
-
 			}
 		}
 	}
-
-
 }
 
 </style>

+ 2 - 2
src/views/login/LoginView.vue

@@ -13,8 +13,8 @@ const errorMsg = ref('');
 const loading = ref(false);
 
 const loginFormData = ref({
-  username: 'admin',
-  password: 'admin123'
+  username: '',
+  password: ''
 })
 
 const handleSubmit = async () => {

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff