|
@@ -0,0 +1,455 @@
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import AutoResizeTextarea from './components/AutoResizeTextarea.vue'
|
|
|
|
|
+import userBubbleLight from './image/userBubbleLight.png'
|
|
|
|
|
+import userBubbleDark from './image/userBubbleDark.png'
|
|
|
|
|
+import robotBubbleLight from './image/robotBubbleLight.png'
|
|
|
|
|
+import robotBubbleDark from './image/robotBubbleDark.png'
|
|
|
|
|
+import { useThemeStore } from '@/stores/modules/theme'
|
|
|
|
|
+
|
|
|
|
|
+const themeStore = useThemeStore()
|
|
|
|
|
+const modalSize = ref('large')
|
|
|
|
|
+const userQuestion = ref()
|
|
|
|
|
+
|
|
|
|
|
+const isFooterInputFocus = ref(false)
|
|
|
|
|
+
|
|
|
|
|
+const userBubbleImg = computed(() => {
|
|
|
|
|
+ return themeStore.theme === 'light' ? userBubbleLight : userBubbleDark
|
|
|
|
|
+})
|
|
|
|
|
+const robotBubbleImg = computed(() => {
|
|
|
|
|
+ return themeStore.theme === 'light' ? robotBubbleLight : robotBubbleDark
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+interface MessageItem {
|
|
|
|
|
+ type: 'robot' | 'user'
|
|
|
|
|
+ content: string
|
|
|
|
|
+ feedback?: 'good' | 'noGood' | '' // 反馈结果
|
|
|
|
|
+ isShowFeedback?: boolean // 是否展示反馈样式
|
|
|
|
|
+ isAnswer?: boolean // 是否为用户问题的答案,是则才能展示反馈组件
|
|
|
|
|
+}
|
|
|
|
|
+const messages = ref<MessageItem[]>([
|
|
|
|
|
+ {
|
|
|
|
|
+ type: 'robot',
|
|
|
|
|
+ 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?'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ 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.'
|
|
|
|
|
+ }
|
|
|
|
|
+])
|
|
|
|
|
+
|
|
|
|
|
+// 用户问题请求时间
|
|
|
|
|
+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 = () => {
|
|
|
|
|
+ if (!userQuestion.value) return
|
|
|
|
|
+
|
|
|
|
|
+ messages.value.push({
|
|
|
|
|
+ type: 'user',
|
|
|
|
|
+ content: userQuestion.value
|
|
|
|
|
+ })
|
|
|
|
|
+ userQuestion.value = ''
|
|
|
|
|
+ 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
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const emit = defineEmits(['close'])
|
|
|
|
|
+// 关闭聊天窗口
|
|
|
|
|
+const handleClose = () => {
|
|
|
|
|
+ progressInterval.value && clearInterval(progressInterval.value)
|
|
|
|
|
+ emit('close')
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="ai-robot" :style="{ width: modalSize === 'large' ? '1000px' : '484px' }">
|
|
|
|
|
+ <div class="top-section">
|
|
|
|
|
+ <div class="header">
|
|
|
|
|
+ <span class="welcome">Hi! I'm your Freight Assistant</span>
|
|
|
|
|
+ <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>
|
|
|
|
|
+ </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"
|
|
|
|
|
+ @mouseenter="msg.isShowFeedback = true"
|
|
|
|
|
+ @mouseleave="msg.isShowFeedback = false"
|
|
|
|
|
+ >
|
|
|
|
|
+ <!-- 请求失败后的提示icon -->
|
|
|
|
|
+ <span
|
|
|
|
|
+ v-if="queryTime === -3 && index === messages.length - 1"
|
|
|
|
|
+ class="font_family icon-icon_warning_fill_b"
|
|
|
|
|
+ style="margin-top: 1px; color: #c9353f"
|
|
|
|
|
+ ></span>
|
|
|
|
|
+ <!-- loading icon -->
|
|
|
|
|
+ <img
|
|
|
|
|
+ class="loading-img"
|
|
|
|
|
+ v-if="queryTime > -1 && queryTime < 120 && index === messages.length - 1"
|
|
|
|
|
+ src="./image/icon_loading.png"
|
|
|
|
|
+ alt=""
|
|
|
|
|
+ />
|
|
|
|
|
+ {{ msg.content }}
|
|
|
|
|
+ <div class="review" v-if="msg.isShowFeedback && msg.isAnswer">
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ v-if="msg.feedback !== 'good'"
|
|
|
|
|
+ class="el-button--text"
|
|
|
|
|
+ @click="msg.feedback = 'good'"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="font_family icon-icon_good_b"></span>
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ <div v-if="msg.feedback === 'good'" 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'"
|
|
|
|
|
+ class="el-button--text"
|
|
|
|
|
+ @click="msg.feedback = 'noGood'"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="font_family icon-icon_notgood_b"></span>
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ <div v-if="msg.feedback === 'noGood'" style="width: 16px; text-align: center">
|
|
|
|
|
+ <span
|
|
|
|
|
+ style="color: var(--color-theme); font-size: 14px"
|
|
|
|
|
+ class="font_family icon-icon_notgood__filled_b"
|
|
|
|
|
+ ></span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <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="" />
|
|
|
|
|
+ <!-- 暂停回答 icon -->
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="pause-btn"
|
|
|
|
|
+ v-if="index === messages.length - 1 && queryTime > 30 && queryTime < 120"
|
|
|
|
|
+ @click="handlePause"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="dot"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </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"
|
|
|
|
|
+ />
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="input-icon"
|
|
|
|
|
+ :class="[userQuestion ? 'input-style' : 'disable']"
|
|
|
|
|
+ @click="handleSend"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="font_family icon-icon_send_b"></span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<style lang="scss" scoped>
|
|
|
|
|
+.ai-robot {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 74px;
|
|
|
|
|
+ right: 24px;
|
|
|
|
|
+ height: calc(100% - 98px);
|
|
|
|
|
+ z-index: 4000;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ border: 1px solid var(--color-border);
|
|
|
|
|
+ box-shadow: 4px 4px 32px 0px rgba(0, 0, 0, 0.2);
|
|
|
|
|
+ background-color: var(--color-dialog-body-bg);
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ .top-section {
|
|
|
|
|
+ height: 150px;
|
|
|
|
|
+ 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%
|
|
|
|
|
+ );
|
|
|
|
|
+ .header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ height: 64px;
|
|
|
|
|
+ padding: 0 16px;
|
|
|
|
|
+ .welcome {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ }
|
|
|
|
|
+ .option-icon {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ .font_family {
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ color: var(--color-theme);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .chat-messages {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 16px;
|
|
|
|
|
+ margin-top: 81px;
|
|
|
|
|
+ padding: 0 16px;
|
|
|
|
|
+ overflow: auto;
|
|
|
|
|
+ .message-item {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ padding: 11px 8px;
|
|
|
|
|
+ margin-bottom: 7px;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ background-color: var(--scoring-bg-color);
|
|
|
|
|
+ .review {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: -24px;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 13px;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 30px;
|
|
|
|
|
+ margin-top: 10px;
|
|
|
|
|
+ padding-left: 30px;
|
|
|
|
|
+ padding-top: 10px;
|
|
|
|
|
+
|
|
|
|
|
+ button.el-button + .el-button {
|
|
|
|
|
+ margin-left: 0px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .review-input-card {
|
|
|
|
|
+ margin-top: 6px;
|
|
|
|
|
+ padding: 8px;
|
|
|
|
|
+ text-align: right;
|
|
|
|
|
+ box-shadow: 1px 1px 12px 0px rgba(0, 0, 0, 0.05);
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-button--text {
|
|
|
|
|
+ height: 16px;
|
|
|
|
|
+ width: 16px;
|
|
|
|
|
+ span {
|
|
|
|
|
+ color: var(--color-neutral-2);
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ }
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ span {
|
|
|
|
|
+ color: var(--color-theme);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .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: var(--color-customize-column-right-section-bg);
|
|
|
|
|
+ .dot {
|
|
|
|
|
+ height: 5px;
|
|
|
|
|
+ width: 5px;
|
|
|
|
|
+ border-radius: 1px;
|
|
|
|
|
+ background-color: var(--color-theme);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .query-style {
|
|
|
|
|
+ color: #b5b9bf;
|
|
|
|
|
+ }
|
|
|
|
|
+ .robot-bubble {
|
|
|
|
|
+ background: var(--scoring-bg-color);
|
|
|
|
|
+ align-self: flex-start;
|
|
|
|
|
+ .robot-bubble-img {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ left: -1px;
|
|
|
|
|
+ bottom: -7px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .user-bubble {
|
|
|
|
|
+ align-self: flex-end;
|
|
|
|
|
+ background: linear-gradient(
|
|
|
|
|
+ to right,
|
|
|
|
|
+ var(--color-ai-user-bubble-bg-gradient-begin),
|
|
|
|
|
+ var(--color-ai-user-bubble-bg-gradient-end)
|
|
|
|
|
+ );
|
|
|
|
|
+ .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 var(--input-border);
|
|
|
|
|
+ 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;
|
|
|
|
|
+ &.disable {
|
|
|
|
|
+ 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);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // .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>
|