Kaynağa Gözat

Merge branch 'dev_zyh' into feat_delivery_zyh

zhouyuhao 6 ay önce
ebeveyn
işleme
0427a53975

+ 76 - 1
src/api/module/AIRobot.ts

@@ -57,4 +57,79 @@ export const PromptAITest = (params: any, config: any) => {
     },
     config
   )
-}
+}
+
+/**
+ * 获取prompt提示器
+ */
+export const getPrompt = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'robot_chat',
+      operate: 'ai_chat_prompt',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * ai聊天
+ */
+export const aiChat = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'robot_chat',
+      operate: 'ai_chat',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 暂停ai聊天
+ */
+export const pauseAiChat = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'robot_chat',
+      operate: 'ai_chat_stop',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 评价ai聊天
+ */
+export const feedbackAiChat = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'robot_chat',
+      operate: 'ai_chat_answer_mark',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * AI Robot预设问题显示
+ */
+export const AIRobotInit = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'robot_chat',
+      operate: 'ai_chat_fixed_init',
+      ...params
+    },
+    config
+  )
+}

+ 11 - 55
src/components/AIRobot/src/AIRobot.vue

@@ -9,62 +9,17 @@ const AIRobotHoverVisible = ref(false)
 const clicked = ref(false)
 const isShowDefault = ref(false)
 const isShowAIRobotTop = ref(false)
-const AIIconVisible = ref(false)
-const DeQuestions = ref([
-  {
-    label: 'List shipments with ETA changes in the last 30 days.',
-    value: 'List shipments with ETA changes in the last 30 days.',
-    isLong: false
-  },
-  {
-    label: 'Shipments arriving in the next 7 days.',
-    value: 'Shipments arriving in the next 7 days.',
-    isLong: false
-  },
-  {
-    label: 'Show shipments delayed in the last 30 days.',
-    value: 'Show shipments delayed in the last 30 days.',
-    isLong: false
-  },
-  {
-    label: 'List shipments with milestone updates in the last 7 days.',
-    value: 'List shipments with milestone updates in the last 7 days.',
-    isLong: false
-  },
-  {
-    label: 'List shipments with milestone in the last 7 days2sdsa.',
-    value: 'List shipments with milestone in the last 7 days2sdsa.',
-    isLong: false
-  },
-  {
-    label: 'List shipments with milestone updates 7 days3fef.',
-    value: 'List shipments with milestone updates 7 days3fef.',
-    isLong: false
-  },
-  {
-    label:
-      'List shipments with ETA changes in the last 30 days11111111111111111111111111111111111.',
-    value:
-      'List shipments with ETA changes in the last 30 days11111111111111111111111111111111111.',
-    isLong: true
-  },
-  {
-    label:
-      'List shipments with ETA changes in the last 30 days1111111111111111111111111111111111122.',
-    value:
-      'List shipments with ETA changes in the last 30 days1111111111111111111111111111111111122.',
-    isLong: true
-  },
-  {
-    label:
-      'List shipments with ETA changes in the last 30 days1111111111111111111111111111111111133.',
-    value:
-      'List shipments with ETA changes in the last 30 days1111111111111111111111111111111111133.',
-    isLong: true
-  }
-])
+const AIIconVisible = ref(true)
+const DeQuestions = ref([])
 const itemGroups = ref([])
 
+const AIRobotInit = () => {
+  $api.AIRobotInit({}).then((res:any) => {
+    DeQuestions.value = res.data.fixed_question
+    prepareGroups()
+  })
+}
+
 // 鼠标hover AIRobot图标
 const AvatarMouseEnter = () => {
   if (clicked.value) {
@@ -91,6 +46,7 @@ const AvatarClick = () => {
 // 隐藏上方弹窗
 const HideAIRobotTop = () => {
   isShowDefault.value = false
+  isShowAIRobotTop.value = true
 }
 
 // 隐藏上方弹窗
@@ -165,7 +121,7 @@ const handelClick = (item: any) => {
 }
 
 onMounted(() => {
-  prepareGroups()
+  AIRobotInit()
   emitter.on('login-success', isShowLogin)
   emitter.on('login-out', Logout)
   emitter.on('checkPrompt', Logout)

+ 4 - 1
src/components/AddRules/src/AddRules.vue

@@ -7,6 +7,7 @@ import ETDShipments from './components/ETDShipments.vue'
 import NotiFrequency from './components/NotiFrequency.vue'
 import NotiMethods from './components/NotiMethods.vue'
 import submitsucessful from './images/icon_success_big@2x.png'
+import moment from 'moment-timezone'
 interface CheckboxItem {
   value: string
   label: string
@@ -363,11 +364,13 @@ const UnsavedCollapse = () => {
 
 // 保存subscribe配置
 const missingmessage = ref('')
+let defaultTimeZone = 'UTC' + moment().tz(moment.tz.guess()).format('Z')
 // 保存成功调用接口
 const SaveSuceessful = () => {
   $api
     .Savesubscribe({
-      ...savesubscribeobj
+      ...savesubscribeobj,
+      default_time_zone: defaultTimeZone
     })
     .then((res: any) => {
       if (res.code === 200) {

+ 4 - 1
src/components/CreateAddRules/src/CreateAddRules.vue

@@ -9,6 +9,7 @@ import ShipmentRange from './components/ShipmentRange.vue'
 import NotiMethods from './components/NotiMethods.vue'
 import submitsucessful from './images/icon_success_big@2x.png'
 import { useRouter } from 'vue-router'
+import moment from 'moment-timezone'
 
 const router = useRouter()
 interface CheckboxItem {
@@ -565,13 +566,15 @@ const closeAirETD = (val: any) => {
 
 // 保存subscribe配置
 const missingmessage = ref('')
+let defaultTimeZone = 'UTC' + moment().tz(moment.tz.guess()).format('Z')
 // 保存成功调用接口
 const SaveSuceessful = () => {
   $api
     .MonitoringSave({
       ...savesubscribeobj,
       is_similar_rule: false,
-      id: editTableidtwo.value
+      id: editTableidtwo.value,
+      default_time_zone: defaultTimeZone
     })
     .then((res: any) => {
       if (res.code === 200) {

+ 1 - 0
src/styles/reset.scss

@@ -137,6 +137,7 @@ div {
 
 .markdown-body {
   display: inline-block;
+  width: 100%;
   line-height: 1.6;
   font-size: 16px;
   color: #333;

+ 128 - 67
src/views/AIRobotChat/src/AIRobotChat.vue

@@ -7,9 +7,11 @@ import userBubbleDark from './image/userBubbleDark.png'
 import robotBubbleLight from './image/robotBubbleLight.png'
 import robotBubbleDark from './image/robotBubbleDark.png'
 import { useThemeStore } from '@/stores/modules/theme'
+import { useUserStore } from '@/stores/modules/user'
 import MarkdownIt from 'markdown-it'
 import 'github-markdown-css/github-markdown.css'
 
+const userStore = useUserStore()
 const md = new MarkdownIt({
   html: true,
   linkify: true,
@@ -18,6 +20,10 @@ const md = new MarkdownIt({
 })
 
 const renderedMessage = (content) => {
+  // console.log('content', content)
+  if (!content) {
+    return ''
+  }
   const normalized = content
     .replace(/\\n/g, '\n') // 粘贴带来的 \\n 处理
     .replace(/\r\n/g, '\n') // Windows 换行标准化
@@ -45,42 +51,27 @@ const isShowHeaderShadow = ref(false)
 
 const loadingAnswer = ref(false) // 是否正在加载答案
 interface MessageItem {
+  id?: string // 唯一标识
   type: 'robot' | 'user'
   content: string
-  feedback?: 'good' | 'noGood' | '' // 反馈结果
+  feedback?: 'Cood' | 'Not Good' | '' // 反馈结果
   isShowFeedback?: boolean // 是否展示反馈样式
   isAnswer?: boolean // 是否为用户问题的答案,是则才能展示反馈组件
   isError?: boolean // 是否为错误消息
   isCancel?: boolean // 是否为取消消息
+  html?: string // 渲染后的HTML内容
 }
 const messages = ref<MessageItem[]>([
   {
     type: 'robot',
     isShowFeedback: false,
     content: 'You can click on Frequently Asked Questions above or type your own question'
-  },
-  {
-    type: 'user',
-    content:
-      'Hi! I am your Freight Assistant. How can I help you?Of course! Please provide me with the details of your shipment.Of course! Please provide me with the details of your shipment.'
-  },
-  {
-    type: 'robot',
-    feedback: '',
-    isShowFeedback: false,
-    isAnswer: true,
-    content: 'Can you help me with my shipment?'
-  },
-  {
-    type: 'user',
-    content:
-      'Of course! Please provide me with the details of your shipment.Of course! Please provide me with the details of your shipment.'
   }
 ])
 messages.value = JSON.parse(sessionStorage.getItem('AIChat')) || messages.value
-onMounted(() => {
+const handleOpen = () => {
   scrollToBottom()
-})
+}
 
 watch(
   () => messages.value,
@@ -106,10 +97,79 @@ const progressStatus = {
 
 const isShowTips = ref(false) // 是否展示提示信息
 
+const parseHtmlString = (data) => {
+  const lines = data.split('\n')
+
+  function streamMarkdown() {
+    const lastMsg: any = messages.value[messages.value.length - 1]
+    let index = 0
+    lastMsg.content = '' // 清空上一个消息的内容
+    const timer = setInterval(() => {
+      if (index < lines.length) {
+        lastMsg.content += lines[index] + '\n'
+        lastMsg.html = renderedMessage(lastMsg.content) // ✅ 每次整段渲染
+        index++
+        scrollToBottom() // 滚动到底部
+      } else {
+        clearInterval(timer)
+      }
+    }, 150)
+  }
+
+  streamMarkdown()
+}
+
 const progressInterval = ref()
-const handleSend = (question, isPresetQuestion = true) => {
-  if (!question) return
+const serial_no = ref()
+const is_FixedAnswer = ref(true) // 是否为预设问题 true是自由问题 false是预设问题
+const aiChat = (question, isPresetQuestion) => {
+  serial_no.value = userStore.userInfo?.uname + Date.now().toString()
+  $api
+    .aiChat({
+      serial_no: serial_no.value,
+      prompt: sessionStorage.getItem('prompt'),
+      question_type: isPresetQuestion || !is_FixedAnswer ? 'Predefined Question' : 'Free Question',
+      // question_type: 'Free Question',
+      question_content: question,
+      fixed_faq: !is_FixedAnswer ? messages.value[messages.value.length - 2].content : ''
+    })
+    .then((res) => {
+      if (isPause.value) {
+        return
+      }
+      if (res.code === 200) {
+        clearInterval(progressInterval.value)
+
+        is_FixedAnswer.value = res.data.is_fixedAnswer_end || false
+        const { data } = res.data
+        messages.value[messages.value.length - 1] = {
+          id: serial_no.value,
+          type: 'robot',
+          content: '',
+          feedback: '',
+          isShowFeedback: false,
+          isAnswer: true
+        }
 
+        parseHtmlString(data)
+        scrollToBottom()
+        loadingAnswer.value = false
+        queryTime.value = -1
+      } else {
+        loadingAnswer.value = false
+        messages.value[messages.value.length - 1].isError = true
+      }
+    })
+}
+const handleSend = (question, isPresetQuestion = true, isExternal = false) => {
+  if (!question) return
+  if (
+    isExternal &&
+    messages.value.length === 1 &&
+    messages.value[0].content === progressStatus.init
+  ) {
+    messages.value.pop()
+  }
   if (loadingAnswer.value) {
     isShowTips.value = true
     setTimeout(() => {
@@ -117,8 +177,11 @@ const handleSend = (question, isPresetQuestion = true) => {
     }, 2000)
     return
   }
+  isPause.value = false
   loadingAnswer.value = true
 
+  aiChat(question, isPresetQuestion)
+  // 将用户内容添加到消息列表
   messages.value.push({
     type: 'user',
     content: question
@@ -181,56 +244,53 @@ function scrollToBottom() {
   })
 }
 
-const simulateStreamingMarkdown = () => {
-  loadingAnswer.value = true
-  const chunks: any = [userQuestion.value]
-
-  messages.value.push({
-    type: 'robot',
-    content: '',
-    isAnswer: true,
-    feedback: '',
-    isShowFeedback: false
-  })
-
-  let index = 0
-
-  autoScroll.value = true
-  scrollToBottom()
-
-  const interval = setInterval(() => {
-    index === chunks.length - 1 ? (loadingAnswer.value = false) : ''
-    if (index < chunks.length) {
-      messages.value[messages.value.length - 1].content += chunks[index]
-      index++
-
-      if (autoScroll.value) {
+const isPause = ref(false) // 是否暂停回答
+// 暂停回答
+const handlePause = () => {
+  $api
+    .pauseAiChat({
+      serial_no: serial_no.value
+    })
+    .then((res) => {
+      if (res.code === 200) {
+        clearInterval(progressInterval.value)
+        is_FixedAnswer.value = true
+        queryTime.value = -1
+        messages.value[messages.value.length - 1].isCancel = true
+        messages.value[messages.value.length - 1].content = progressStatus.cancel
+        isPause.value = true
+        loadingAnswer.value = false
         scrollToBottom()
       }
-    } else {
-      clearInterval(interval)
-      userQuestion.value = ''
-    }
-  }, 200) // 每500ms追加一段
+    })
 }
 
-// 暂停回答
-const handlePause = () => {
-  clearInterval(progressInterval.value)
-  queryTime.value = -1
-  messages.value[messages.value.length - 1].isCancel = true
-  messages.value[messages.value.length - 1].content = progressStatus.cancel
+// 评价
+const handleFeedback = (index, feedback) => {
+  const message = messages.value[index]
+  message.feedback = feedback
+  $api
+    .feedbackAiChat({
+      serial_no: message.id,
+      feedback: feedback
+    })
+    .then((res) => {
+      if (res.code === 200) {
+        messages.value[index].isShowFeedback = false
+      }
+    })
 }
 
 const emit = defineEmits(['close'])
 // 关闭聊天窗口
 const handleClose = () => {
-  progressInterval.value && clearInterval(progressInterval.value)
+  // progressInterval.value && clearInterval(progressInterval.value)
   emit('close')
 }
 
 defineExpose({
-  handleSend
+  handleSend,
+  handleOpen
 })
 </script>
 
@@ -289,33 +349,35 @@ defineExpose({
           src="./image/icon_loading.png"
           alt=""
         />
-        <div v-html="renderedMessage(msg.content)" class="markdown-body"></div>
+        <div style="display: inline-block; max-width: 100%">
+          <div v-html="msg.html || renderedMessage(msg.content)" class="markdown-body"></div>
+        </div>
         <LoadingDots
           v-if="index === messages.length - 1 && msg.isAnswer && loadingAnswer"
         ></LoadingDots>
         <!-- 评价  -->
         <div class="review" v-if="msg.isShowFeedback && msg.isAnswer">
           <el-button
-            v-if="msg.feedback !== 'good'"
+            v-if="msg.feedback !== 'Cood'"
             class="el-button--text"
-            @click="msg.feedback = 'good'"
+            @click="handleFeedback(index, 'Cood')"
           >
             <span class="font_family icon-icon_good_b"></span>
           </el-button>
-          <div v-if="msg.feedback === 'good'" style="width: 16px; text-align: center">
+          <div v-if="msg.feedback === 'Cood'" style="width: 16px; text-align: center">
             <span
               style="color: var(--color-theme); font-size: 14px"
               class="font_family icon-icon_good__filled_b"
             ></span>
           </div>
           <el-button
-            v-if="msg.feedback !== 'noGood'"
+            v-if="msg.feedback !== 'Not Good'"
             class="el-button--text"
-            @click="msg.feedback = 'noGood'"
+            @click="handleFeedback(index, 'Not Good')"
           >
             <span class="font_family icon-icon_notgood_b"></span>
           </el-button>
-          <div v-if="msg.feedback === 'noGood'" style="width: 16px; text-align: center">
+          <div v-if="msg.feedback === 'Not Good'" style="width: 16px; text-align: center">
             <span
               style="color: var(--color-theme); font-size: 14px"
               class="font_family icon-icon_notgood__filled_b"
@@ -352,7 +414,6 @@ defineExpose({
           @focus="isFooterInputFocus = true"
           @blur="isFooterInputFocus = false"
         />
-        <el-button @click="simulateStreamingMarkdown">测试</el-button>
         <div
           class="input-icon"
           :class="[!userQuestion || queryTime !== -1 ? 'disable' : '']"

+ 10 - 57
src/views/AIRobotChat/src/components/AIQuestions.vue

@@ -5,67 +5,22 @@ const props = defineProps<{
   modalSize: String
 }>()
 
-const DeQuestions = ref([
-  {
-    label: 'List shipments with ETA changes in the last 30 days.',
-    value: 'List shipments with ETA changes in the last 30 days.',
-    isLong: false
-  },
-  {
-    label: 'Shipments arriving in the next 7 days.',
-    value: 'Shipments arriving in the next 7 days.',
-    isLong: false
-  },
-  {
-    label: 'Show shipments delayed in the last 30 days.',
-    value: 'Show shipments delayed in the last 30 days.',
-    isLong: false
-  },
-  {
-    label: 'List shipments with milestone updates in the last 7 days.',
-    value: 'List shipments with milestone updates in the last 7 days.',
-    isLong: false
-  },
-  {
-    label: 'List shipments with milestone in the last 7 days2sdsa.',
-    value: 'List shipments with milestone in the last 7 days2sdsa.',
-    isLong: false
-  },
-  {
-    label: 'List shipments with milestone updates 7 days3fef.',
-    value: 'List shipments with milestone updates 7 days3fef.',
-    isLong: false
-  },
-  {
-    label:
-      'List shipments with ETA changes in the last 30 days11111111111111111111111111111111111.',
-    value:
-      'List shipments with ETA changes in the last 30 days11111111111111111111111111111111111.',
-    isLong: true
-  },
-  {
-    label:
-      'List shipments with ETA changes in the last 30 days1111111111111111111111111111111111122.',
-    value:
-      'List shipments with ETA changes in the last 30 days1111111111111111111111111111111111122.',
-    isLong: true
-  },
-  {
-    label:
-      'List shipments with ETA changes in the last 30 days1111111111111111111111111111111111133.',
-    value:
-      'List shipments with ETA changes in the last 30 days1111111111111111111111111111111111133.',
-    isLong: true
-  }
-])
+const DeQuestions = ref([])
 const itemGroups = ref([])
 const pages = ref([])
+
+const AIRobotInit = () => {
+  $api.AIRobotInit({}).then((res:any) => {
+    DeQuestions.value = res.data.fixed_question
+    prepareGroups()
+    generatePages()
+  })
+}
 // 随机显示方法
 const prepareGroups = () => {
   const groups = []
   let currentGroup = []
   let currentHeight = 0
-
   DeQuestions.value.forEach((item) => {
     const itemHeight = item.isLong ? 2 : 1
 
@@ -83,7 +38,6 @@ const prepareGroups = () => {
   if (currentGroup.length > 0) {
     groups.push(currentGroup)
   }
-
   itemGroups.value = groups
 }
 // 智能分页算法
@@ -141,8 +95,7 @@ const generatePages = () => {
   pages.value = result.filter((p) => p.row1.length > 0)
 }
 onMounted(() => {
-  prepareGroups()
-  generatePages()
+  AIRobotInit()
 })
 
 const emit = defineEmits<{ question: [string] }>()

+ 16 - 5
src/views/Layout/src/LayoutView.vue

@@ -18,12 +18,12 @@ const handleMenuCollapse = (val: boolean) => {
 const isShowAIRobotChat = ref(false)
 const handleColseRobotChat = () => {
   isShowAIRobotChat.value = false
+  AIRobotChatref.value?.handleOpen()
 }
-const onClick = () => {
-  isShowAIRobotChat.value = true
-}
+
 const AvatarClick = () => {
   isShowAIRobotChat.value = true
+  AIRobotChatref.value?.handleOpen()
 }
 
 const AIRobotChatref = ref()
@@ -32,9 +32,20 @@ const handelClickAIDefault = async (item: any) => {
   isShowAIRobotChat.value = true
   await nextTick()
   if (AIRobotChatref.value) {
-    AIRobotChatref.value.handleSend(item)
+    AIRobotChatref.value.handleSend(item, true, true)
   }
 }
+
+const getPrompt = () => {
+  $api.getPrompt().then((res) => {
+    if (res.code === 200) {
+      sessionStorage.setItem('prompt', JSON.stringify(res.data.prompt))
+    }
+  })
+}
+onMounted(() => {
+  getPrompt()
+})
 </script>
 <template>
   <el-container class="layout-container">
@@ -67,7 +78,7 @@ const handelClickAIDefault = async (item: any) => {
     <AIRobot @AvatarClick="AvatarClick" @handelClickAIDefault="handelClickAIDefault"></AIRobot>
     <AIRobotChat
       ref="AIRobotChatref"
-      v-if="isShowAIRobotChat"
+      v-show="isShowAIRobotChat"
       @close="handleColseRobotChat"
     ></AIRobotChat>
     <ScoringGrade></ScoringGrade>