|
|
@@ -34,6 +34,11 @@ const robotBubbleImg = computed(() => {
|
|
|
return themeStore.theme === 'light' ? robotBubbleLight : robotBubbleDark
|
|
|
})
|
|
|
|
|
|
+// 是否显示footer的顶部阴影
|
|
|
+const isShowFooterShadow = ref(false)
|
|
|
+// 是否显示header的底部阴影
|
|
|
+const isShowHeaderShadow = ref(false)
|
|
|
+
|
|
|
const loadingAnswer = ref(false) // 是否正在加载答案
|
|
|
interface MessageItem {
|
|
|
type: 'robot' | 'user'
|
|
|
@@ -41,6 +46,8 @@ interface MessageItem {
|
|
|
feedback?: 'good' | 'noGood' | '' // 反馈结果
|
|
|
isShowFeedback?: boolean // 是否展示反馈样式
|
|
|
isAnswer?: boolean // 是否为用户问题的答案,是则才能展示反馈组件
|
|
|
+ isError?: boolean // 是否为错误消息
|
|
|
+ isCancel?: boolean // 是否为取消消息
|
|
|
}
|
|
|
const messages = ref<MessageItem[]>([
|
|
|
{
|
|
|
@@ -64,6 +71,10 @@ const messages = ref<MessageItem[]>([
|
|
|
content: 'Of course! Please provide me with the details of your shipment.'
|
|
|
}
|
|
|
])
|
|
|
+messages.value = JSON.parse(sessionStorage.getItem('AIChat')) || messages.value
|
|
|
+onMounted(() => {
|
|
|
+ scrollToBottom()
|
|
|
+})
|
|
|
|
|
|
watch(
|
|
|
() => messages.value,
|
|
|
@@ -87,22 +98,55 @@ const progressStatus = {
|
|
|
cancel: 'You have stopped this answer'
|
|
|
}
|
|
|
|
|
|
+const isShowTips = ref(false) // 是否展示提示信息
|
|
|
+
|
|
|
const progressInterval = ref()
|
|
|
const handleSend = (question, isPresetQuestion = true) => {
|
|
|
if (!question) return
|
|
|
|
|
|
+ if (loadingAnswer.value) {
|
|
|
+ isShowTips.value = true
|
|
|
+ setTimeout(() => {
|
|
|
+ isShowTips.value = false
|
|
|
+ }, 2000)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ loadingAnswer.value = true
|
|
|
+
|
|
|
messages.value.push({
|
|
|
type: 'user',
|
|
|
content: question
|
|
|
})
|
|
|
- !isPresetQuestion ? (question = '') : ''
|
|
|
+ !isPresetQuestion ? (userQuestion.value = '') : ''
|
|
|
queryTime.value = 0
|
|
|
messages.value.push({
|
|
|
type: 'robot',
|
|
|
content: progressStatus[0]
|
|
|
})
|
|
|
+ autoScroll.value = true
|
|
|
+ scrollToBottom()
|
|
|
+
|
|
|
progressInterval.value = setInterval(() => {
|
|
|
queryTime.value++
|
|
|
+ // 定义时间点与对应状态的映射
|
|
|
+ const timeToStatusMap = {
|
|
|
+ 15: 15,
|
|
|
+ 30: 30,
|
|
|
+ 60: 60
|
|
|
+ }
|
|
|
+ // 获取最后一个消息对象
|
|
|
+ const lastMessage = messages.value[messages.value.length - 1]
|
|
|
+ if (queryTime.value === 120) {
|
|
|
+ clearInterval(progressInterval.value)
|
|
|
+ lastMessage.content = progressStatus[120]
|
|
|
+ lastMessage.isError = true
|
|
|
+ queryTime.value = -1
|
|
|
+ loadingAnswer.value = false
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (timeToStatusMap[queryTime.value] !== undefined) {
|
|
|
+ lastMessage.content = progressStatus[queryTime.value]
|
|
|
+ }
|
|
|
}, 1000)
|
|
|
}
|
|
|
const showScrollButton = ref(false) // 控制按钮显示
|
|
|
@@ -116,6 +160,12 @@ function handleScroll() {
|
|
|
autoScroll.value = atBottom
|
|
|
|
|
|
showScrollButton.value = el.scrollHeight > el.clientHeight && !autoScroll.value
|
|
|
+
|
|
|
+ isShowFooterShadow.value =
|
|
|
+ messagesRef.value?.scrollTop <
|
|
|
+ messagesRef.value?.scrollHeight - messagesRef.value?.clientHeight - 1
|
|
|
+
|
|
|
+ isShowHeaderShadow.value = !!messagesRef.value?.scrollTop
|
|
|
}
|
|
|
function scrollToBottom() {
|
|
|
nextTick(() => {
|
|
|
@@ -126,6 +176,7 @@ function scrollToBottom() {
|
|
|
}
|
|
|
|
|
|
const simulateStreamingMarkdown = () => {
|
|
|
+ loadingAnswer.value = true
|
|
|
const chunks = [
|
|
|
'# 欢迎使用 Markdown\n\n',
|
|
|
'这是一个用于测试的 **Markdown** 文本。\n\n',
|
|
|
@@ -135,10 +186,7 @@ const simulateStreamingMarkdown = () => {
|
|
|
'- 非常适合逐行流式显示\n\n',
|
|
|
'这是一段测试!\n'
|
|
|
]
|
|
|
- const clientHeight = messagesRef.value.getBoundingClientRect()
|
|
|
- console.log(messagesRef.value.offsetHeight, 'messagesRef.value', clientHeight)
|
|
|
|
|
|
- loadingAnswer
|
|
|
messages.value.push({
|
|
|
type: 'robot',
|
|
|
content: '',
|
|
|
@@ -146,12 +194,12 @@ const simulateStreamingMarkdown = () => {
|
|
|
feedback: '',
|
|
|
isShowFeedback: false
|
|
|
})
|
|
|
- loadingAnswer.value = true
|
|
|
+
|
|
|
let index = 0
|
|
|
|
|
|
- if (autoScroll.value) {
|
|
|
- scrollToBottom()
|
|
|
- }
|
|
|
+ autoScroll.value = true
|
|
|
+ scrollToBottom()
|
|
|
+
|
|
|
const interval = setInterval(() => {
|
|
|
index === chunks.length - 1 ? (loadingAnswer.value = false) : ''
|
|
|
if (index < chunks.length) {
|
|
|
@@ -164,43 +212,14 @@ const simulateStreamingMarkdown = () => {
|
|
|
} else {
|
|
|
clearInterval(interval)
|
|
|
}
|
|
|
- }, 500) // 每500ms追加一段
|
|
|
-}
|
|
|
-
|
|
|
-// 根据时间更新消息内容
|
|
|
-const updateMessageContent = (time) => {
|
|
|
- const lastMessageIndex = messages.value.length - 1
|
|
|
-
|
|
|
- // 确保消息数组不为空
|
|
|
- if (lastMessageIndex >= 0) {
|
|
|
- messages.value[lastMessageIndex].content = progressStatus[time]
|
|
|
- if (time === 120) {
|
|
|
- clearInterval(progressInterval.value)
|
|
|
- queryTime.value = -3
|
|
|
- }
|
|
|
- }
|
|
|
+ }, 200) // 每500ms追加一段
|
|
|
}
|
|
|
-watch(
|
|
|
- () => queryTime.value,
|
|
|
- (newVal) => {
|
|
|
- // 定义时间点与对应状态的映射
|
|
|
- const timeToStatusMap = {
|
|
|
- 15: 15,
|
|
|
- 30: 30,
|
|
|
- 60: 60,
|
|
|
- 120: 120
|
|
|
- }
|
|
|
|
|
|
- // 如果当前时间点在映射中,更新消息内容
|
|
|
- if (timeToStatusMap[newVal] !== undefined) {
|
|
|
- updateMessageContent(timeToStatusMap[newVal])
|
|
|
- }
|
|
|
- }
|
|
|
-)
|
|
|
// 暂停回答
|
|
|
const handlePause = () => {
|
|
|
clearInterval(progressInterval.value)
|
|
|
- queryTime.value = -2
|
|
|
+ queryTime.value = -1
|
|
|
+ messages.value[messages.value.length - 1].isCancel = true
|
|
|
messages.value[messages.value.length - 1].content = progressStatus.cancel
|
|
|
}
|
|
|
|
|
|
@@ -214,7 +233,7 @@ const handleClose = () => {
|
|
|
|
|
|
<template>
|
|
|
<div class="ai-robot" :style="{ width: modalSize === 'large' ? '1000px' : '484px' }">
|
|
|
- <div class="top-section">
|
|
|
+ <div class="top-section" :class="[isShowHeaderShadow ? 'box-shadow' : '']">
|
|
|
<div class="header">
|
|
|
<span class="welcome">Hi! I'm your Freight Assistant</span>
|
|
|
<div class="option-icon">
|
|
|
@@ -232,18 +251,19 @@ const handleClose = () => {
|
|
|
</div>
|
|
|
</div>
|
|
|
<AIQuestions :modalSize="modalSize" @question="handleSend"></AIQuestions>
|
|
|
+ <div class="warning-tips" v-if="isShowTips">
|
|
|
+ <div class="warning-bg">
|
|
|
+ <span class="warning-icon font_family icon-icon_warning_fill_b"></span>
|
|
|
+ </div>
|
|
|
+ <span>Answer in progress, please wait.</span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- <div
|
|
|
- class="chat-messages"
|
|
|
- ref="messagesRef"
|
|
|
- @scroll="handleScroll"
|
|
|
- :style="{ marginTop: modalSize === 'large' ? '81px' : '151px' }"
|
|
|
- >
|
|
|
+ <div class="chat-messages" ref="messagesRef" @scroll="handleScroll">
|
|
|
<div
|
|
|
class="message-item"
|
|
|
:class="[
|
|
|
msg.type === 'user' ? 'user-bubble' : 'robot-bubble',
|
|
|
- ((queryTime > -1 && queryTime < 120) || queryTime === -2) && index === messages.length - 1
|
|
|
+ (queryTime > -1 && queryTime < 120 && index === messages.length - 1) || msg.isCancel
|
|
|
? 'query-style'
|
|
|
: ''
|
|
|
]"
|
|
|
@@ -252,11 +272,10 @@ const handleClose = () => {
|
|
|
@mouseenter="msg.isShowFeedback = true"
|
|
|
@mouseleave="msg.isShowFeedback = false"
|
|
|
>
|
|
|
- <!-- 请求失败后的提示icon -->
|
|
|
- <span
|
|
|
- v-if="queryTime === -3 && index === messages.length - 1"
|
|
|
- class="pause-icon font_family icon-icon_warning_fill_b"
|
|
|
- ></span>
|
|
|
+ <!-- 请求失败后的提示icon -->
|
|
|
+ <div class="pause-bg" v-if="msg.isError">
|
|
|
+ <span class="pause-icon font_family icon-icon_warning_fill_b"></span>
|
|
|
+ </div>
|
|
|
<!-- loading icon -->
|
|
|
<img
|
|
|
class="loading-img"
|
|
|
@@ -307,7 +326,7 @@ const handleClose = () => {
|
|
|
<!-- 暂停回答 icon -->
|
|
|
<div
|
|
|
class="pause-btn"
|
|
|
- v-if="index === messages.length - 1 && queryTime > 30 && queryTime < 120"
|
|
|
+ v-if="index === messages.length - 1 && queryTime > 29 && queryTime < 120"
|
|
|
@click="handlePause"
|
|
|
>
|
|
|
<div class="dot"></div>
|
|
|
@@ -319,20 +338,26 @@ const handleClose = () => {
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <div class="footer-input" :class="[isFooterInputFocus ? 'focus-style' : '']">
|
|
|
- <AutoResizeTextarea
|
|
|
- v-model="userQuestion"
|
|
|
- :placeholder="'Type your question here...'"
|
|
|
- @focus="isFooterInputFocus = true"
|
|
|
- @blur="isFooterInputFocus = false"
|
|
|
- />
|
|
|
- <el-button @click="simulateStreamingMarkdown">测试</el-button>
|
|
|
- <div
|
|
|
- class="input-icon"
|
|
|
- :class="[userQuestion ? 'input-style' : 'disable']"
|
|
|
- @click="handleSend(userQuestion, false)"
|
|
|
- >
|
|
|
- <span class="font_family icon-icon_send_b"></span>
|
|
|
+ <div
|
|
|
+ class="footer"
|
|
|
+ :class="[isFooterInputFocus ? 'focus-style' : '', isShowFooterShadow ? 'box-shadow' : '']"
|
|
|
+ >
|
|
|
+ <div class="footer-input">
|
|
|
+ <AutoResizeTextarea
|
|
|
+ style="flex: 1"
|
|
|
+ v-model="userQuestion"
|
|
|
+ :placeholder="'Type your question here...'"
|
|
|
+ @focus="isFooterInputFocus = true"
|
|
|
+ @blur="isFooterInputFocus = false"
|
|
|
+ />
|
|
|
+ <el-button @click="simulateStreamingMarkdown">测试</el-button>
|
|
|
+ <div
|
|
|
+ class="input-icon"
|
|
|
+ :class="[!userQuestion || queryTime !== -1 ? 'disable' : '']"
|
|
|
+ @click="handleSend(userQuestion, false)"
|
|
|
+ >
|
|
|
+ <span class="font_family icon-icon_send_b"></span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -353,18 +378,23 @@ const handleClose = () => {
|
|
|
background-color: var(--color-dialog-body-bg);
|
|
|
overflow: hidden;
|
|
|
.top-section {
|
|
|
- height: 150px;
|
|
|
+ position: relative;
|
|
|
+ padding-bottom: 16px;
|
|
|
background: linear-gradient(
|
|
|
to bottom,
|
|
|
var(--color-ai-chat-header-bg-gradient-begin) 10%,
|
|
|
var(--color-ai-chat-header-bg-gradient-begin) 10%,
|
|
|
var(--color-ai-chat-header-bg-gradient-end) 100%
|
|
|
);
|
|
|
+ &.box-shadow {
|
|
|
+ box-shadow: -2px 0px 12px rgba(0, 0, 0, 0.1);
|
|
|
+ }
|
|
|
.header {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
height: 64px;
|
|
|
+ margin-bottom: 6px;
|
|
|
padding: 0 16px;
|
|
|
.welcome {
|
|
|
font-size: 18px;
|
|
|
@@ -383,13 +413,45 @@ const handleClose = () => {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+ .warning-tips {
|
|
|
+ position: absolute;
|
|
|
+ top: 60px;
|
|
|
+ left: 17px;
|
|
|
+ width: calc(100% - 32px);
|
|
|
+ height: 40px;
|
|
|
+ padding-top: 14px;
|
|
|
+ background-color: var(--color-warning-tips-bg);
|
|
|
+ border-radius: 6px;
|
|
|
+ text-align: center;
|
|
|
+ .warning-bg {
|
|
|
+ position: relative;
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 12px;
|
|
|
+ width: 12px;
|
|
|
+ margin-right: 6px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background-color: #fff;
|
|
|
+ }
|
|
|
+ .warning-icon {
|
|
|
+ position: absolute;
|
|
|
+ top: -3px;
|
|
|
+ border-radius: 50%;
|
|
|
+ border: none;
|
|
|
+ }
|
|
|
+ span {
|
|
|
+ color: #edb82f;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
+
|
|
|
.chat-messages {
|
|
|
flex: 1;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: 16px;
|
|
|
- padding: 0 16px 16px;
|
|
|
+ padding: 0 16px 18px;
|
|
|
overflow: auto;
|
|
|
.message-item {
|
|
|
position: relative;
|
|
|
@@ -415,14 +477,21 @@ const handleClose = () => {
|
|
|
margin-left: 0px;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+ .pause-bg {
|
|
|
+ position: relative;
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 12px;
|
|
|
+ width: 12px;
|
|
|
+ margin-right: 6px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background-color: #fff;
|
|
|
+ }
|
|
|
.pause-icon {
|
|
|
- height: 16px;
|
|
|
- width: 16px;
|
|
|
- margin-top: 1px;
|
|
|
- margin-right: 4px;
|
|
|
+ position: absolute;
|
|
|
+ top: -2px;
|
|
|
color: #c9353f;
|
|
|
- background-color: #fff;
|
|
|
border-radius: 50%;
|
|
|
border: none;
|
|
|
}
|
|
|
@@ -500,18 +569,44 @@ const handleClose = () => {
|
|
|
position: absolute;
|
|
|
right: 50%;
|
|
|
transform: translateX(50%);
|
|
|
- bottom: 52px;
|
|
|
+ bottom: 58px;
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
align-items: center;
|
|
|
height: 32px;
|
|
|
width: 32px;
|
|
|
border-radius: 50%;
|
|
|
- background-color: var(--color-customize-column-right-section-bg);
|
|
|
+ background-color: #f5f4f4;
|
|
|
+ box-shadow: 2px 2px 12px 0px rgba(0, 0, 0, 0.1);
|
|
|
+ span {
|
|
|
+ color: #2b2f36;
|
|
|
+ }
|
|
|
cursor: pointer;
|
|
|
&:hover {
|
|
|
background-color: var(--color-theme);
|
|
|
- color: #fff;
|
|
|
+ span {
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .footer {
|
|
|
+ padding: 12px 16px;
|
|
|
+ &.box-shadow {
|
|
|
+ box-shadow: 0px 2px 12px rgba(0, 0, 0, 0.1);
|
|
|
+ }
|
|
|
+ &.focus-style {
|
|
|
+ .footer-input {
|
|
|
+ border: 1px solid var(--color-theme);
|
|
|
+ }
|
|
|
+ .input-icon {
|
|
|
+ background-color: var(--color-theme);
|
|
|
+ span {
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+ &:hover {
|
|
|
+ background-color: #d56200;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -521,10 +616,10 @@ const handleClose = () => {
|
|
|
gap: 12px;
|
|
|
padding: 4px 12px;
|
|
|
padding-right: 4px;
|
|
|
- margin: 12px 16px;
|
|
|
border: 1px solid var(--input-border);
|
|
|
border-radius: 20px;
|
|
|
box-sizing: border-box;
|
|
|
+
|
|
|
.input-icon {
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
@@ -538,18 +633,6 @@ const handleClose = () => {
|
|
|
cursor: not-allowed;
|
|
|
}
|
|
|
}
|
|
|
- .input-style {
|
|
|
- background-color: var(--color-theme);
|
|
|
- span {
|
|
|
- color: #fff;
|
|
|
- }
|
|
|
- &:hover {
|
|
|
- background-color: #d56200;
|
|
|
- }
|
|
|
- }
|
|
|
- &.focus-style {
|
|
|
- border: 1px solid var(--color-theme);
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
@keyframes loading-rotate {
|