|
@@ -7,9 +7,12 @@ import userBubbleDark from './image/userBubbleDark.png'
|
|
|
import robotBubbleLight from './image/robotBubbleLight.png'
|
|
import robotBubbleLight from './image/robotBubbleLight.png'
|
|
|
import robotBubbleDark from './image/robotBubbleDark.png'
|
|
import robotBubbleDark from './image/robotBubbleDark.png'
|
|
|
import { useThemeStore } from '@/stores/modules/theme'
|
|
import { useThemeStore } from '@/stores/modules/theme'
|
|
|
|
|
+import { useUserStore } from '@/stores/modules/user'
|
|
|
import MarkdownIt from 'markdown-it'
|
|
import MarkdownIt from 'markdown-it'
|
|
|
import 'github-markdown-css/github-markdown.css'
|
|
import 'github-markdown-css/github-markdown.css'
|
|
|
|
|
|
|
|
|
|
+const userStore = useUserStore()
|
|
|
|
|
+const AIQuestion = ref()
|
|
|
const md = new MarkdownIt({
|
|
const md = new MarkdownIt({
|
|
|
html: true,
|
|
html: true,
|
|
|
linkify: true,
|
|
linkify: true,
|
|
@@ -18,6 +21,9 @@ const md = new MarkdownIt({
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const renderedMessage = (content) => {
|
|
const renderedMessage = (content) => {
|
|
|
|
|
+ if (!content) {
|
|
|
|
|
+ return ''
|
|
|
|
|
+ }
|
|
|
const normalized = content
|
|
const normalized = content
|
|
|
.replace(/\\n/g, '\n') // 粘贴带来的 \\n 处理
|
|
.replace(/\\n/g, '\n') // 粘贴带来的 \\n 处理
|
|
|
.replace(/\r\n/g, '\n') // Windows 换行标准化
|
|
.replace(/\r\n/g, '\n') // Windows 换行标准化
|
|
@@ -43,44 +49,29 @@ const isShowFooterShadow = ref(false)
|
|
|
// 是否显示header的底部阴影
|
|
// 是否显示header的底部阴影
|
|
|
const isShowHeaderShadow = ref(false)
|
|
const isShowHeaderShadow = ref(false)
|
|
|
|
|
|
|
|
-const loadingAnswer = ref(false) // 是否正在加载答案
|
|
|
|
|
|
|
+const loadingAnswer = ref(false) // 是否正在请求答案
|
|
|
interface MessageItem {
|
|
interface MessageItem {
|
|
|
|
|
+ id?: string // 唯一标识
|
|
|
type: 'robot' | 'user'
|
|
type: 'robot' | 'user'
|
|
|
content: string
|
|
content: string
|
|
|
- feedback?: 'good' | 'noGood' | '' // 反馈结果
|
|
|
|
|
|
|
+ feedback?: 'Cood' | 'Not Good' | '' // 反馈结果
|
|
|
isShowFeedback?: boolean // 是否展示反馈样式
|
|
isShowFeedback?: boolean // 是否展示反馈样式
|
|
|
isAnswer?: boolean // 是否为用户问题的答案,是则才能展示反馈组件
|
|
isAnswer?: boolean // 是否为用户问题的答案,是则才能展示反馈组件
|
|
|
isError?: boolean // 是否为错误消息
|
|
isError?: boolean // 是否为错误消息
|
|
|
isCancel?: boolean // 是否为取消消息
|
|
isCancel?: boolean // 是否为取消消息
|
|
|
|
|
+ html?: string // 渲染后的HTML内容
|
|
|
}
|
|
}
|
|
|
const messages = ref<MessageItem[]>([
|
|
const messages = ref<MessageItem[]>([
|
|
|
{
|
|
{
|
|
|
type: 'robot',
|
|
type: 'robot',
|
|
|
isShowFeedback: false,
|
|
isShowFeedback: false,
|
|
|
content: 'You can click on Frequently Asked Questions above or type your own question'
|
|
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
|
|
messages.value = JSON.parse(sessionStorage.getItem('AIChat')) || messages.value
|
|
|
-onMounted(() => {
|
|
|
|
|
|
|
+const handleOpen = () => {
|
|
|
scrollToBottom()
|
|
scrollToBottom()
|
|
|
-})
|
|
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
watch(
|
|
watch(
|
|
|
() => messages.value,
|
|
() => messages.value,
|
|
@@ -106,10 +97,98 @@ const progressStatus = {
|
|
|
|
|
|
|
|
const isShowTips = ref(false) // 是否展示提示信息
|
|
const isShowTips = ref(false) // 是否展示提示信息
|
|
|
|
|
|
|
|
|
|
+const isShowLoadingDots = ref(false) // 是否展示加载点(加载答案)
|
|
|
|
|
+const parseHtmlString = (data) => {
|
|
|
|
|
+ if (!data) {
|
|
|
|
|
+ isShowLoadingDots.value = false // 停止显示加载点
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const lines = data.split('\n')
|
|
|
|
|
+ isShowLoadingDots.value = true // 开始显示加载点
|
|
|
|
|
+ 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 {
|
|
|
|
|
+ isShowLoadingDots.value = false // 停止显示加载点
|
|
|
|
|
+ clearInterval(timer)
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 150)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ streamMarkdown()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
const progressInterval = ref()
|
|
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) => {
|
|
|
|
|
+ AIQuestion.value.AIRobotInit()
|
|
|
|
|
+ serial_no.value = userStore.userInfo?.uname + Date.now().toString()
|
|
|
|
|
+ let fixed_faq = ''
|
|
|
|
|
+ if (!is_FixedAnswer.value) {
|
|
|
|
|
+ fixed_faq = messages.value[messages.value.length - 3].content
|
|
|
|
|
+ } else if (isPresetQuestion) {
|
|
|
|
|
+ fixed_faq = question
|
|
|
|
|
+ }
|
|
|
|
|
+ $api
|
|
|
|
|
+ .aiChat({
|
|
|
|
|
+ serial_no: serial_no.value,
|
|
|
|
|
+ prompt: sessionStorage.getItem('prompt'),
|
|
|
|
|
+ question_type:
|
|
|
|
|
+ isPresetQuestion || !is_FixedAnswer.value ? 'Predefined Question' : 'Free Question',
|
|
|
|
|
+ question_content: question,
|
|
|
|
|
+ fixed_faq: fixed_faq
|
|
|
|
|
+ })
|
|
|
|
|
+ .then((res) => {
|
|
|
|
|
+ if (isPause.value || queryTime.value === -1) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ clearInterval(progressInterval.value)
|
|
|
|
|
+
|
|
|
|
|
+ is_FixedAnswer.value =
|
|
|
|
|
+ res.data.is_fixedAnswer_end !== null || res.data.is_fixedAnswer_end !== undefined
|
|
|
|
|
+ ? res.data.is_fixedAnswer_end
|
|
|
|
|
+ : true
|
|
|
|
|
+ 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
|
|
|
|
|
+ messages.value[messages.value.length - 1].content = progressStatus[120]
|
|
|
|
|
+ clearInterval(progressInterval.value)
|
|
|
|
|
+ queryTime.value = -1
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+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) {
|
|
if (loadingAnswer.value) {
|
|
|
isShowTips.value = true
|
|
isShowTips.value = true
|
|
|
setTimeout(() => {
|
|
setTimeout(() => {
|
|
@@ -117,14 +196,18 @@ const handleSend = (question, isPresetQuestion = true) => {
|
|
|
}, 2000)
|
|
}, 2000)
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
+ isPause.value = false
|
|
|
loadingAnswer.value = true
|
|
loadingAnswer.value = true
|
|
|
-
|
|
|
|
|
|
|
+ // 将用户内容添加到消息列表
|
|
|
messages.value.push({
|
|
messages.value.push({
|
|
|
type: 'user',
|
|
type: 'user',
|
|
|
- content: question
|
|
|
|
|
|
|
+ content: question,
|
|
|
|
|
+ isAnswer: true
|
|
|
})
|
|
})
|
|
|
!isPresetQuestion ? (userQuestion.value = '') : ''
|
|
!isPresetQuestion ? (userQuestion.value = '') : ''
|
|
|
queryTime.value = 0
|
|
queryTime.value = 0
|
|
|
|
|
+
|
|
|
|
|
+ aiChat(question, isPresetQuestion)
|
|
|
messages.value.push({
|
|
messages.value.push({
|
|
|
type: 'robot',
|
|
type: 'robot',
|
|
|
content: progressStatus[0]
|
|
content: progressStatus[0]
|
|
@@ -155,6 +238,7 @@ const handleSend = (question, isPresetQuestion = true) => {
|
|
|
}
|
|
}
|
|
|
}, 1000)
|
|
}, 1000)
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
const showScrollButton = ref(false) // 控制按钮显示
|
|
const showScrollButton = ref(false) // 控制按钮显示
|
|
|
const messagesRef = ref()
|
|
const messagesRef = ref()
|
|
|
const autoScroll = ref(true)
|
|
const autoScroll = ref(true)
|
|
@@ -173,7 +257,8 @@ function handleScroll() {
|
|
|
|
|
|
|
|
isShowHeaderShadow.value = !!messagesRef.value?.scrollTop
|
|
isShowHeaderShadow.value = !!messagesRef.value?.scrollTop
|
|
|
}
|
|
}
|
|
|
-function scrollToBottom() {
|
|
|
|
|
|
|
+const scrollToBottom = (isScroll = false) => {
|
|
|
|
|
+ if (!isScroll && (!autoScroll.value || !messagesRef.value)) return
|
|
|
nextTick(() => {
|
|
nextTick(() => {
|
|
|
if (messagesRef.value) {
|
|
if (messagesRef.value) {
|
|
|
messagesRef.value.scrollTop = messagesRef.value.scrollHeight
|
|
messagesRef.value.scrollTop = messagesRef.value.scrollHeight
|
|
@@ -181,57 +266,85 @@ 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()
|
|
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
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+// 判断是否展示评价icon
|
|
|
|
|
+const shouldShowFeedback = (msg, index) => {
|
|
|
|
|
+ if (index === messages.value.length - 1 && isShowLoadingDots.value) {
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+ return msg.type === 'robot' && msg.isAnswer
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const emit = defineEmits(['close'])
|
|
const emit = defineEmits(['close'])
|
|
|
// 关闭聊天窗口
|
|
// 关闭聊天窗口
|
|
|
const handleClose = () => {
|
|
const handleClose = () => {
|
|
|
- progressInterval.value && clearInterval(progressInterval.value)
|
|
|
|
|
|
|
+ // progressInterval.value && clearInterval(progressInterval.value)
|
|
|
emit('close')
|
|
emit('close')
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+const clearData = () => {
|
|
|
|
|
+ messages.value = [
|
|
|
|
|
+ {
|
|
|
|
|
+ type: 'robot',
|
|
|
|
|
+ isShowFeedback: false,
|
|
|
|
|
+ content: 'You can click on Frequently Asked Questions above or type your own question'
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ progressInterval.value && clearInterval(progressInterval.value)
|
|
|
|
|
+ sessionStorage.removeItem('AIChat')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const liabilityExeDialog = ref(false) // 免责声明弹窗
|
|
|
|
|
+const handleLiabilityExeDialog = () => {
|
|
|
|
|
+ liabilityExeDialog.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handelclckaiinit = () => {
|
|
|
|
|
+ AIQuestion.value.AIRobotInit()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
defineExpose({
|
|
defineExpose({
|
|
|
- handleSend
|
|
|
|
|
|
|
+ handleSend,
|
|
|
|
|
+ handleOpen,
|
|
|
|
|
+ clearData,
|
|
|
|
|
+ handelclckaiinit
|
|
|
})
|
|
})
|
|
|
|
|
+
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
|
<template>
|
|
@@ -240,20 +353,38 @@ defineExpose({
|
|
|
<div class="header">
|
|
<div class="header">
|
|
|
<span class="welcome">Hi! I'm your Freight Assistant</span>
|
|
<span class="welcome">Hi! I'm your Freight Assistant</span>
|
|
|
<div class="option-icon">
|
|
<div class="option-icon">
|
|
|
- <span
|
|
|
|
|
- v-if="modalSize === 'large'"
|
|
|
|
|
- class="font_family icon-icon_sidebar__window_b"
|
|
|
|
|
- @click="modalSize = 'small'"
|
|
|
|
|
- ></span>
|
|
|
|
|
- <span
|
|
|
|
|
- v-else-if="modalSize !== 'large'"
|
|
|
|
|
- class="font_family icon-icon_maximized__window_b"
|
|
|
|
|
- @click="modalSize = 'large'"
|
|
|
|
|
- ></span>
|
|
|
|
|
- <span @click="handleClose" class="font_family icon-icon_collapsed__to_widget_b"></span>
|
|
|
|
|
|
|
+ <el-tooltip v-if="modalSize === 'large'" trigger="hover" content="Sidebar Window">
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ style="width: 24px; height: 24px"
|
|
|
|
|
+ class="el-button--text"
|
|
|
|
|
+ @click="modalSize = 'small'"
|
|
|
|
|
+ ><span class="font_family icon-icon_sidebar__window_b"></span
|
|
|
|
|
+ ></el-button>
|
|
|
|
|
+ </el-tooltip>
|
|
|
|
|
+
|
|
|
|
|
+ <el-tooltip v-else-if="modalSize !== 'large'" trigger="hover" content="Maximized Window">
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ style="width: 24px; height: 24px"
|
|
|
|
|
+ class="el-button--text"
|
|
|
|
|
+ @click="modalSize = 'large'"
|
|
|
|
|
+ ><span class="font_family icon-icon_maximized__window_b"></span
|
|
|
|
|
+ ></el-button>
|
|
|
|
|
+ </el-tooltip>
|
|
|
|
|
+
|
|
|
|
|
+ <el-tooltip trigger="hover" content="Collapsed to Widget">
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ style="width: 24px; height: 24px"
|
|
|
|
|
+ class="el-button--text"
|
|
|
|
|
+ @click="handleClose"
|
|
|
|
|
+ ><span
|
|
|
|
|
+ @click="handleClose"
|
|
|
|
|
+ class="font_family icon-icon_collapsed__to_widget_b"
|
|
|
|
|
+ ></span
|
|
|
|
|
+ ></el-button>
|
|
|
|
|
+ </el-tooltip>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- <AIQuestions :modalSize="modalSize" @question="handleSend"></AIQuestions>
|
|
|
|
|
|
|
+ <AIQuestions ref="AIQuestion" :modalSize="modalSize" @question="handleSend"></AIQuestions>
|
|
|
<div class="warning-tips" v-if="isShowTips">
|
|
<div class="warning-tips" v-if="isShowTips">
|
|
|
<div class="warning-bg">
|
|
<div class="warning-bg">
|
|
|
<span class="warning-icon font_family icon-icon_warning_fill_b"></span>
|
|
<span class="warning-icon font_family icon-icon_warning_fill_b"></span>
|
|
@@ -289,39 +420,42 @@ defineExpose({
|
|
|
src="./image/icon_loading.png"
|
|
src="./image/icon_loading.png"
|
|
|
alt=""
|
|
alt=""
|
|
|
/>
|
|
/>
|
|
|
- <!-- <span v-if="!msg.isAnswer">{{ msg.content }}</span> -->
|
|
|
|
|
- <div>
|
|
|
|
|
- <div v-html="renderedMessage(msg.content)" class="markdown-body"></div>
|
|
|
|
|
- <LoadingDots
|
|
|
|
|
- v-if="index === messages.length - 1 && msg.isAnswer && loadingAnswer"
|
|
|
|
|
- ></LoadingDots>
|
|
|
|
|
- <div></div>
|
|
|
|
|
|
|
+ <div style="display: inline-block; max-width: 100%">
|
|
|
|
|
+ <div v-html="msg.html || renderedMessage(msg.content)" class="markdown-body"></div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <LoadingDots
|
|
|
|
|
+ v-if="
|
|
|
|
|
+ index === messages.length - 1 &&
|
|
|
|
|
+ msg.isAnswer &&
|
|
|
|
|
+ isShowLoadingDots &&
|
|
|
|
|
+ msg.type === 'robot'
|
|
|
|
|
+ "
|
|
|
|
|
+ ></LoadingDots>
|
|
|
<!-- 评价 -->
|
|
<!-- 评价 -->
|
|
|
- <div class="review" v-if="msg.isShowFeedback && msg.isAnswer">
|
|
|
|
|
|
|
+ <div class="review" v-if="shouldShowFeedback(msg, index)">
|
|
|
<el-button
|
|
<el-button
|
|
|
- v-if="msg.feedback !== 'good'"
|
|
|
|
|
|
|
+ v-if="msg.feedback !== 'Cood'"
|
|
|
class="el-button--text"
|
|
class="el-button--text"
|
|
|
- @click="msg.feedback = 'good'"
|
|
|
|
|
|
|
+ @click="handleFeedback(index, 'Cood')"
|
|
|
>
|
|
>
|
|
|
<span class="font_family icon-icon_good_b"></span>
|
|
<span class="font_family icon-icon_good_b"></span>
|
|
|
</el-button>
|
|
</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
|
|
<span
|
|
|
- style="color: var(--color-theme); font-size: 14px"
|
|
|
|
|
|
|
+ style="color: var(--color-theme)"
|
|
|
class="font_family icon-icon_good__filled_b"
|
|
class="font_family icon-icon_good__filled_b"
|
|
|
></span>
|
|
></span>
|
|
|
</div>
|
|
</div>
|
|
|
<el-button
|
|
<el-button
|
|
|
- v-if="msg.feedback !== 'noGood'"
|
|
|
|
|
|
|
+ v-if="msg.feedback !== 'Not Good'"
|
|
|
class="el-button--text"
|
|
class="el-button--text"
|
|
|
- @click="msg.feedback = 'noGood'"
|
|
|
|
|
|
|
+ @click="handleFeedback(index, 'Not Good')"
|
|
|
>
|
|
>
|
|
|
<span class="font_family icon-icon_notgood_b"></span>
|
|
<span class="font_family icon-icon_notgood_b"></span>
|
|
|
</el-button>
|
|
</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
|
|
<span
|
|
|
- style="color: var(--color-theme); font-size: 14px"
|
|
|
|
|
|
|
+ style="color: var(--color-theme)"
|
|
|
class="font_family icon-icon_notgood__filled_b"
|
|
class="font_family icon-icon_notgood__filled_b"
|
|
|
></span>
|
|
></span>
|
|
|
</div>
|
|
</div>
|
|
@@ -330,16 +464,18 @@ defineExpose({
|
|
|
<img class="robot-bubble-img" v-if="msg.type === 'robot'" :src="robotBubbleImg" alt="" />
|
|
<img class="robot-bubble-img" v-if="msg.type === 'robot'" :src="robotBubbleImg" alt="" />
|
|
|
<img class="user-bubble-img" v-else-if="msg.type === 'user'" :src="userBubbleImg" alt="" />
|
|
<img class="user-bubble-img" v-else-if="msg.type === 'user'" :src="userBubbleImg" alt="" />
|
|
|
<!-- 暂停回答 icon -->
|
|
<!-- 暂停回答 icon -->
|
|
|
- <div
|
|
|
|
|
- class="pause-btn"
|
|
|
|
|
|
|
+ <el-tooltip
|
|
|
v-if="index === messages.length - 1 && queryTime > 29 && queryTime < 120"
|
|
v-if="index === messages.length - 1 && queryTime > 29 && queryTime < 120"
|
|
|
- @click="handlePause"
|
|
|
|
|
- >
|
|
|
|
|
- <div class="dot"></div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ content="Cancel Answer"
|
|
|
|
|
+ placement="bottom-start"
|
|
|
|
|
+ effect="dark"
|
|
|
|
|
+ ><div class="pause-btn" @click="handlePause">
|
|
|
|
|
+ <div class="dot"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-tooltip>
|
|
|
</div>
|
|
</div>
|
|
|
<!-- 滚动到底部icon -->
|
|
<!-- 滚动到底部icon -->
|
|
|
- <div v-if="showScrollButton" class="scroll-to-bottom-btn" @click="scrollToBottom">
|
|
|
|
|
|
|
+ <div v-if="showScrollButton" class="scroll-to-bottom-btn" @click="scrollToBottom(true)">
|
|
|
<span class="font_family icon-icon_movedown_b"></span>
|
|
<span class="font_family icon-icon_movedown_b"></span>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -356,7 +492,6 @@ defineExpose({
|
|
|
@focus="isFooterInputFocus = true"
|
|
@focus="isFooterInputFocus = true"
|
|
|
@blur="isFooterInputFocus = false"
|
|
@blur="isFooterInputFocus = false"
|
|
|
/>
|
|
/>
|
|
|
- <el-button @click="simulateStreamingMarkdown">测试</el-button>
|
|
|
|
|
<div
|
|
<div
|
|
|
class="input-icon"
|
|
class="input-icon"
|
|
|
:class="[!userQuestion || queryTime !== -1 ? 'disable' : '']"
|
|
:class="[!userQuestion || queryTime !== -1 ? 'disable' : '']"
|
|
@@ -365,17 +500,61 @@ defineExpose({
|
|
|
<span class="font_family icon-icon_send_b"></span>
|
|
<span class="font_family icon-icon_send_b"></span>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <div class="liability-exemption">
|
|
|
|
|
+ <span>Content is generated by Al, please check carefully!</span>
|
|
|
|
|
+ <span class="liability-exemption-btn" @click="handleLiabilityExeDialog">Disclaimer</span>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <el-dialog
|
|
|
|
|
+ class="liability-exemption-dialog"
|
|
|
|
|
+ v-model="liabilityExeDialog"
|
|
|
|
|
+ title="Disclaimer"
|
|
|
|
|
+ width="800"
|
|
|
|
|
+ top="20vh"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="title"
|
|
|
|
|
+ style="margin-bottom: 8px; font-size: 16px; font-weight: 700; line-height: 24px"
|
|
|
|
|
+ >
|
|
|
|
|
+ Important Notice: AI-Generated Content
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <p>
|
|
|
|
|
+ This chat assistant is powered by artificial intelligence (AI) and is designed to help you
|
|
|
|
|
+ access information and answer your queries efficiently. Please be aware of the following:
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p>
|
|
|
|
|
+ <strong>• AI-Generated Responses: </strong>All responses are automatically generated by AI.
|
|
|
|
|
+ While we strive for accuracy, errors or inaccuracies may occur. Please verify critical
|
|
|
|
|
+ information independently before making business decisions.
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p>
|
|
|
|
|
+ <strong>• Data Privacy & Security:</strong> You can only access shipment data within your
|
|
|
|
|
+ authorized account permissions. Your data remains confidential and will not be shared with
|
|
|
|
|
+ other users or third parties.
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p>
|
|
|
|
|
+ <strong>• Information Accuracy: </strong> For critical business decisions or time-sensitive
|
|
|
|
|
+ matters, we recommend contacting our customer service team directly for verification.
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p>
|
|
|
|
|
+ <strong>• Service Limitations: </strong> This assistant provides general guidance and data
|
|
|
|
|
+ queries. For complex or specialized requests, please reach out to our support team.
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p>
|
|
|
|
|
+ By using this AI assistant, you acknowledge these limitations and agree to use the
|
|
|
|
|
+ information provided accordingly.
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </el-dialog>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
<style lang="scss" scoped>
|
|
|
.ai-robot {
|
|
.ai-robot {
|
|
|
position: absolute;
|
|
position: absolute;
|
|
|
- top: 74px;
|
|
|
|
|
|
|
+ top: 24px;
|
|
|
right: 24px;
|
|
right: 24px;
|
|
|
- height: calc(100% - 98px);
|
|
|
|
|
- z-index: 4000;
|
|
|
|
|
|
|
+ height: calc(100% - 48px);
|
|
|
|
|
+ z-index: 2000;
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
border-radius: 12px;
|
|
border-radius: 12px;
|
|
@@ -409,7 +588,6 @@ defineExpose({
|
|
|
.option-icon {
|
|
.option-icon {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- gap: 6px;
|
|
|
|
|
.font_family {
|
|
.font_family {
|
|
|
font-size: 16px;
|
|
font-size: 16px;
|
|
|
cursor: pointer;
|
|
cursor: pointer;
|
|
@@ -462,22 +640,15 @@ defineExpose({
|
|
|
.message-item {
|
|
.message-item {
|
|
|
position: relative;
|
|
position: relative;
|
|
|
display: inline-block;
|
|
display: inline-block;
|
|
|
- padding: 11px 8px;
|
|
|
|
|
|
|
+ padding: 12px 16px;
|
|
|
margin-bottom: 7px;
|
|
margin-bottom: 7px;
|
|
|
border-radius: 12px;
|
|
border-radius: 12px;
|
|
|
background-color: var(--scoring-bg-color);
|
|
background-color: var(--scoring-bg-color);
|
|
|
.review {
|
|
.review {
|
|
|
- position: absolute;
|
|
|
|
|
- bottom: -24px;
|
|
|
|
|
- left: 0;
|
|
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- gap: 13px;
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 30px;
|
|
|
|
|
- margin-top: 10px;
|
|
|
|
|
- padding-left: 30px;
|
|
|
|
|
- padding-top: 5px;
|
|
|
|
|
|
|
+ gap: 16px;
|
|
|
|
|
+ margin-top: 12px;
|
|
|
|
|
|
|
|
button.el-button + .el-button {
|
|
button.el-button + .el-button {
|
|
|
margin-left: 0px;
|
|
margin-left: 0px;
|
|
@@ -506,7 +677,6 @@ defineExpose({
|
|
|
width: 16px;
|
|
width: 16px;
|
|
|
span {
|
|
span {
|
|
|
color: var(--color-neutral-2);
|
|
color: var(--color-neutral-2);
|
|
|
- font-size: 14px;
|
|
|
|
|
}
|
|
}
|
|
|
&:hover {
|
|
&:hover {
|
|
|
span {
|
|
span {
|
|
@@ -526,14 +696,15 @@ defineExpose({
|
|
|
.pause-btn {
|
|
.pause-btn {
|
|
|
position: absolute;
|
|
position: absolute;
|
|
|
right: -22px;
|
|
right: -22px;
|
|
|
- top: 13px;
|
|
|
|
|
|
|
+ top: 50%;
|
|
|
|
|
+ transform: translateY(-50%);
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
justify-content: center;
|
|
|
height: 16px;
|
|
height: 16px;
|
|
|
width: 16px;
|
|
width: 16px;
|
|
|
border-radius: 50%;
|
|
border-radius: 50%;
|
|
|
- background-color: var(--color-customize-column-right-section-bg);
|
|
|
|
|
|
|
+ background-color: var(--color-pause-btn-bg);
|
|
|
.dot {
|
|
.dot {
|
|
|
height: 5px;
|
|
height: 5px;
|
|
|
width: 5px;
|
|
width: 5px;
|
|
@@ -542,11 +713,7 @@ defineExpose({
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- .query-style {
|
|
|
|
|
- span {
|
|
|
|
|
- color: #b5b9bf;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+
|
|
|
.robot-bubble {
|
|
.robot-bubble {
|
|
|
background: var(--scoring-bg-color);
|
|
background: var(--scoring-bg-color);
|
|
|
align-self: flex-start;
|
|
align-self: flex-start;
|
|
@@ -573,7 +740,7 @@ defineExpose({
|
|
|
position: absolute;
|
|
position: absolute;
|
|
|
right: 50%;
|
|
right: 50%;
|
|
|
transform: translateX(50%);
|
|
transform: translateX(50%);
|
|
|
- bottom: 58px;
|
|
|
|
|
|
|
+ bottom: 76px;
|
|
|
display: flex;
|
|
display: flex;
|
|
|
justify-content: center;
|
|
justify-content: center;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
@@ -583,7 +750,7 @@ defineExpose({
|
|
|
// background-color: #f5f4f4;
|
|
// background-color: #f5f4f4;
|
|
|
background: rgba(255, 255, 255, 0.6); /* 半透明背景色 */
|
|
background: rgba(255, 255, 255, 0.6); /* 半透明背景色 */
|
|
|
box-shadow: 2px 2px 12px 0px rgba(0, 0, 0, 0.3);
|
|
box-shadow: 2px 2px 12px 0px rgba(0, 0, 0, 0.3);
|
|
|
- backdrop-filter: blur(1px); /* 应用10px的模糊效果 */
|
|
|
|
|
|
|
+ backdrop-filter: blur(1.7px); /* 应用10px的模糊效果 */
|
|
|
span {
|
|
span {
|
|
|
color: #2b2f36;
|
|
color: #2b2f36;
|
|
|
}
|
|
}
|
|
@@ -640,6 +807,20 @@ defineExpose({
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ .liability-exemption {
|
|
|
|
|
+ margin-top: 8px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ span {
|
|
|
|
|
+ color: var(--color-neutral-3);
|
|
|
|
|
+ }
|
|
|
|
|
+ .liability-exemption-btn {
|
|
|
|
|
+ margin-left: 2px;
|
|
|
|
|
+ text-decoration: underline;
|
|
|
|
|
+ color: var(--color-theme);
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
@keyframes loading-rotate {
|
|
@keyframes loading-rotate {
|
|
|
0% {
|
|
0% {
|
|
@@ -651,4 +832,17 @@ defineExpose({
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+:deep(.liability-exemption-dialog) {
|
|
|
|
|
+ padding-bottom: 0;
|
|
|
|
|
+ .el-dialog__body {
|
|
|
|
|
+ padding-top: 8px;
|
|
|
|
|
+ p {
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ line-height: 21px;
|
|
|
|
|
+ &:last-child {
|
|
|
|
|
+ margin-bottom: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|