|
@@ -1,9 +1,115 @@
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
const modalSize = ref('large')
|
|
const modalSize = ref('large')
|
|
|
|
|
+
|
|
|
|
|
+const inputVModel = ref()
|
|
|
|
|
+
|
|
|
|
|
+const isFooterInputFocus = ref(false)
|
|
|
|
|
+const textareaRef = ref(null)
|
|
|
|
|
+// 实现自适应高度(最多 4 行)
|
|
|
|
|
+const resize = () => {
|
|
|
|
|
+ const el = textareaRef.value
|
|
|
|
|
+ if (!el) return
|
|
|
|
|
+
|
|
|
|
|
+ el.style.height = 'auto' // 先清空旧高度
|
|
|
|
|
+ const scrollHeight = el.scrollHeight
|
|
|
|
|
+
|
|
|
|
|
+ const maxHeight = 100 // 四行时高度
|
|
|
|
|
+
|
|
|
|
|
+ if (scrollHeight <= maxHeight) {
|
|
|
|
|
+ el.style.overflowY = 'hidden'
|
|
|
|
|
+ el.style.height = scrollHeight + 'px'
|
|
|
|
|
+ } else {
|
|
|
|
|
+ el.style.overflowY = 'auto'
|
|
|
|
|
+ el.style.height = maxHeight + 'px'
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 初始挂载和内容变化都触发 resize
|
|
|
|
|
+onMounted(() => nextTick(resize))
|
|
|
|
|
+const messages = ref([
|
|
|
|
|
+ {
|
|
|
|
|
+ type: 'robot',
|
|
|
|
|
+ content: 'Hello! How can I assist you today?'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ type: 'user',
|
|
|
|
|
+ content: 'Hi! I am your Freight Assistant. How can I help you?'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ type: 'robot',
|
|
|
|
|
+ content: 'Can you help me with my shipment?'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ type: 'user',
|
|
|
|
|
+ content: 'Of course! Please provide me with the details of your shipment.'
|
|
|
|
|
+ }
|
|
|
|
|
+])
|
|
|
|
|
+
|
|
|
|
|
+// 用户问题请求时间
|
|
|
|
|
+const queryTime = ref(-1)
|
|
|
|
|
+// 当前用户问题回复进度
|
|
|
|
|
+const progressStatus = {
|
|
|
|
|
+ init: 'You can click on Frequently Asked Questions above or type your own question',
|
|
|
|
|
+ '0': 'Thinking about your question...',
|
|
|
|
|
+ '15': 'Searching for relevant data, please wait...',
|
|
|
|
|
+ '30': 'This query is complex and may take more time',
|
|
|
|
|
+ '60': 'You may try simplifying your question or selecting a Frequently Asked Question',
|
|
|
|
|
+ '120': 'Sorry, the query failed. Please try again later or select a Frequently Asked Question',
|
|
|
|
|
+ cancel: 'You have stopped this answer'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const progressInterval = ref()
|
|
|
|
|
+const handleSend = () => {
|
|
|
|
|
+ queryTime.value = 0
|
|
|
|
|
+ messages.value.push({
|
|
|
|
|
+ type: 'robot',
|
|
|
|
|
+ content: progressStatus[0]
|
|
|
|
|
+ })
|
|
|
|
|
+ progressInterval.value = setInterval(() => {
|
|
|
|
|
+ queryTime.value++
|
|
|
|
|
+ }, 1000)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 根据时间更新消息内容
|
|
|
|
|
+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
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+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
|
|
|
|
|
+ messages.value[messages.value.length - 1].content = progressStatus.cancel
|
|
|
|
|
+}
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
|
<template>
|
|
|
- <div class="ai-robot">
|
|
|
|
|
|
|
+ <div class="ai-robot" :style="{ width: modalSize === 'large' ? '1000px' : '484px' }">
|
|
|
<div class="top-section">
|
|
<div class="top-section">
|
|
|
<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>
|
|
@@ -22,6 +128,62 @@ const modalSize = ref('large')
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <div class="chat-messages" ref="messagesRef">
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="message-item"
|
|
|
|
|
+ :class="[
|
|
|
|
|
+ msg.type === 'user' ? 'user-bubble' : 'robot-bubble',
|
|
|
|
|
+ ((queryTime > -1 && queryTime < 120) || queryTime === -2) && index === messages.length - 1
|
|
|
|
|
+ ? 'query-style'
|
|
|
|
|
+ : ''
|
|
|
|
|
+ ]"
|
|
|
|
|
+ v-for="(msg, index) in messages"
|
|
|
|
|
+ :key="index"
|
|
|
|
|
+ >
|
|
|
|
|
+ <img
|
|
|
|
|
+ class="loading-img"
|
|
|
|
|
+ v-if="queryTime > -1 && queryTime < 120 && index === messages.length - 1"
|
|
|
|
|
+ src="./image/icon_loading.png"
|
|
|
|
|
+ alt=""
|
|
|
|
|
+ />
|
|
|
|
|
+ {{ msg.content }}
|
|
|
|
|
+ <img
|
|
|
|
|
+ class="robot-bubble-img"
|
|
|
|
|
+ v-if="msg.type === 'robot'"
|
|
|
|
|
+ src="./image/robotBubbleLight.png"
|
|
|
|
|
+ alt=""
|
|
|
|
|
+ />
|
|
|
|
|
+ <img
|
|
|
|
|
+ class="user-bubble-img"
|
|
|
|
|
+ v-else-if="msg.type === 'user'"
|
|
|
|
|
+ src="./image/userBubbleLight.png"
|
|
|
|
|
+ alt=""
|
|
|
|
|
+ />
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="pause-btn"
|
|
|
|
|
+ v-if="index === messages.length && queryTime > 30 && queryTime < 120"
|
|
|
|
|
+ @click="handlePause"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="dot"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="footer-input" :class="[isFooterInputFocus ? 'focus-style' : '']">
|
|
|
|
|
+ <textarea
|
|
|
|
|
+ ref="textareaRef"
|
|
|
|
|
+ v-model="inputVModel"
|
|
|
|
|
+ class="input-area"
|
|
|
|
|
+ rows="1"
|
|
|
|
|
+ placeholder="Type your question here..."
|
|
|
|
|
+ @input="resize"
|
|
|
|
|
+ @focus="isFooterInputFocus = true"
|
|
|
|
|
+ @blur="isFooterInputFocus = false"
|
|
|
|
|
+ />
|
|
|
|
|
+ <div class="input-icon" :class="[inputVModel ? 'input-style' : '']" @click="handleSend">
|
|
|
|
|
+ <span class="font_family icon-icon_send_b"></span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
@@ -30,9 +192,10 @@ const modalSize = ref('large')
|
|
|
position: absolute;
|
|
position: absolute;
|
|
|
top: 74px;
|
|
top: 74px;
|
|
|
right: 24px;
|
|
right: 24px;
|
|
|
- width: 1000px;
|
|
|
|
|
height: calc(100% - 98px);
|
|
height: calc(100% - 98px);
|
|
|
z-index: 4000;
|
|
z-index: 4000;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
border-radius: 12px;
|
|
border-radius: 12px;
|
|
|
box-shadow: 4px 4px 32px 0px rgba(0, 0, 0, 0.2);
|
|
box-shadow: 4px 4px 32px 0px rgba(0, 0, 0, 0.2);
|
|
|
background-color: #fff;
|
|
background-color: #fff;
|
|
@@ -64,5 +227,128 @@ const modalSize = ref('large')
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ .chat-messages {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 16px;
|
|
|
|
|
+ margin-top: 81px;
|
|
|
|
|
+ padding: 0 16px;
|
|
|
|
|
+ .message-item {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ padding: 11px 8px;
|
|
|
|
|
+ margin-bottom: 7px;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ .loading-img {
|
|
|
|
|
+ width: 16px;
|
|
|
|
|
+ height: 16px;
|
|
|
|
|
+ margin-top: -1px;
|
|
|
|
|
+ margin-right: 2px;
|
|
|
|
|
+ animation: loading-rotate 2s linear infinite;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .pause-btn {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ right: -22px;
|
|
|
|
|
+ top: 13px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ height: 16px;
|
|
|
|
|
+ width: 16px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background-color: #fff1e6;
|
|
|
|
|
+ .dot {
|
|
|
|
|
+ height: 5px;
|
|
|
|
|
+ width: 5px;
|
|
|
|
|
+ border-radius: 1px;
|
|
|
|
|
+ background-color: var(--color-theme);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .query-style {
|
|
|
|
|
+ color: #b5b9bf;
|
|
|
|
|
+ }
|
|
|
|
|
+ .robot-bubble {
|
|
|
|
|
+ background: #f2f4f7;
|
|
|
|
|
+ align-self: flex-start;
|
|
|
|
|
+ .robot-bubble-img {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ left: -1px;
|
|
|
|
|
+ bottom: -7px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .user-bubble {
|
|
|
|
|
+ align-self: flex-end;
|
|
|
|
|
+ background: linear-gradient(to right, #ffede6, #f2f4f7);
|
|
|
|
|
+ .user-bubble-img {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ bottom: -7px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .footer-input {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: flex-end;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ padding: 4px 12px;
|
|
|
|
|
+ padding-right: 4px;
|
|
|
|
|
+ margin: 12px 16px;
|
|
|
|
|
+ border: 1px solid #eaebed;
|
|
|
|
|
+ border-radius: 20px;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ .input-icon {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ height: 32px;
|
|
|
|
|
+ width: 32px;
|
|
|
|
|
+ padding: 1px 0 0 2px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ }
|
|
|
|
|
+ .input-style {
|
|
|
|
|
+ background-color: var(--color-theme);
|
|
|
|
|
+ span {
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ }
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ background-color: #d56200;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .focus-style {
|
|
|
|
|
+ border-color: var(--color-theme);
|
|
|
|
|
+ }
|
|
|
|
|
+ .input-area {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ line-height: 21px;
|
|
|
|
|
+ padding: 4px;
|
|
|
|
|
+ resize: none;
|
|
|
|
|
+ overflow-y: hidden; // 默认不显示滚动条
|
|
|
|
|
+ height: 40px; // 初始高度(1 行)
|
|
|
|
|
+ max-height: 100px; // 最多 4 行
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ outline-color: #fff;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ transition: height 0.1s ease;
|
|
|
|
|
+ &::placeholder {
|
|
|
|
|
+ color: #b5b9bf;
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ @keyframes loading-rotate {
|
|
|
|
|
+ 0% {
|
|
|
|
|
+ transform: rotate(0deg);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 100% {
|
|
|
|
|
+ transform: rotate(360deg);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
</style>
|
|
</style>
|