|
|
@@ -0,0 +1,654 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import AutoResizeTextarea from './components/AutoResizeTextarea.vue'
|
|
|
+import LoadingDots from './components/LoadingDots.vue'
|
|
|
+import AIQuestions from './components/AIQuestions.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'
|
|
|
+import MarkdownIt from 'markdown-it'
|
|
|
+import 'github-markdown-css/github-markdown.css'
|
|
|
+
|
|
|
+const md = new MarkdownIt({
|
|
|
+ html: true,
|
|
|
+ linkify: true,
|
|
|
+ typographer: true,
|
|
|
+ breaks: true
|
|
|
+})
|
|
|
+
|
|
|
+const renderedMessage = (content) => {
|
|
|
+ return md.render(content)
|
|
|
+}
|
|
|
+
|
|
|
+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
|
|
|
+})
|
|
|
+
|
|
|
+// 是否显示footer的顶部阴影
|
|
|
+const isShowFooterShadow = ref(false)
|
|
|
+// 是否显示header的底部阴影
|
|
|
+const isShowHeaderShadow = ref(false)
|
|
|
+
|
|
|
+const loadingAnswer = ref(false) // 是否正在加载答案
|
|
|
+interface MessageItem {
|
|
|
+ type: 'robot' | 'user'
|
|
|
+ content: string
|
|
|
+ feedback?: 'good' | 'noGood' | '' // 反馈结果
|
|
|
+ isShowFeedback?: boolean // 是否展示反馈样式
|
|
|
+ isAnswer?: boolean // 是否为用户问题的答案,是则才能展示反馈组件
|
|
|
+ isError?: boolean // 是否为错误消息
|
|
|
+ isCancel?: boolean // 是否为取消消息
|
|
|
+}
|
|
|
+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?'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 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.'
|
|
|
+ }
|
|
|
+])
|
|
|
+messages.value = JSON.parse(sessionStorage.getItem('AIChat')) || messages.value
|
|
|
+onMounted(() => {
|
|
|
+ scrollToBottom()
|
|
|
+})
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => messages.value,
|
|
|
+ (newVal) => {
|
|
|
+ // 将消息存储到 sessionStorage 中
|
|
|
+ sessionStorage.setItem('AIChat', JSON.stringify(newVal))
|
|
|
+ },
|
|
|
+ { immediate: true, deep: true }
|
|
|
+)
|
|
|
+
|
|
|
+// 用户问题请求时间
|
|
|
+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 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 ? (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) // 控制按钮显示
|
|
|
+const messagesRef = ref()
|
|
|
+const autoScroll = ref(true)
|
|
|
+
|
|
|
+function handleScroll() {
|
|
|
+ const el = messagesRef.value
|
|
|
+ const threshold = 50 // 到底部的距离阈值
|
|
|
+ const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < threshold
|
|
|
+ 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(() => {
|
|
|
+ if (messagesRef.value) {
|
|
|
+ messagesRef.value.scrollTop = messagesRef.value.scrollHeight
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const simulateStreamingMarkdown = () => {
|
|
|
+ loadingAnswer.value = true
|
|
|
+ const chunks = [
|
|
|
+ '# 欢迎使用这是一个用于测试的 Markdown 文本 Markdown\n\n',
|
|
|
+ '这是一个用于测试的这是一个用于测试的 Markdown 文本 **Markdown** 文本。\n\n',
|
|
|
+ '你可以使用这是一个用于测试的 Markdown 文本 *斜体* 来强调重点。\n\n',
|
|
|
+ '也可以使用这是一个用于测试的 Markdown 文本 [链接](https://example.com) 引导用户跳转。\n\n',
|
|
|
+ '- 支持无这是一个用于测试的 Markdown 文本这是一个用于测试的 Markdown 文本序列表\n',
|
|
|
+ '- 非常适合这是一个用于测试的 Markdown 文本这是一个用于测试的 Markdown 文本这是一个用于测试的 Markdown 文本逐行流式显示\n\n',
|
|
|
+ '这是一段这是一个用于测试的 Markdown 文本这是一个用于测试的 Markdown 文本这是一个用于测试的 Markdown 文本测试!\n'
|
|
|
+ ]
|
|
|
+
|
|
|
+ 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) {
|
|
|
+ scrollToBottom()
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ clearInterval(interval)
|
|
|
+ }
|
|
|
+ }, 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 emit = defineEmits(['close'])
|
|
|
+// 关闭聊天窗口
|
|
|
+const handleClose = () => {
|
|
|
+ progressInterval.value && clearInterval(progressInterval.value)
|
|
|
+ emit('close')
|
|
|
+}
|
|
|
+
|
|
|
+defineExpose({
|
|
|
+ handleSend
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div class="ai-robot" :style="{ width: modalSize === 'large' ? '1000px' : '484px' }">
|
|
|
+ <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">
|
|
|
+ <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>
|
|
|
+ <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">
|
|
|
+ <div
|
|
|
+ class="message-item"
|
|
|
+ :class="[
|
|
|
+ msg.type === 'user' ? 'user-bubble' : 'robot-bubble',
|
|
|
+ (queryTime > -1 && queryTime < 120 && index === messages.length - 1) || msg.isCancel
|
|
|
+ ? 'query-style'
|
|
|
+ : ''
|
|
|
+ ]"
|
|
|
+ v-for="(msg, index) in messages"
|
|
|
+ :key="index"
|
|
|
+ @mouseenter="msg.isShowFeedback = true"
|
|
|
+ @mouseleave="msg.isShowFeedback = false"
|
|
|
+ >
|
|
|
+ <!-- 请求失败后的提示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"
|
|
|
+ v-if="queryTime > -1 && queryTime < 120 && index === messages.length - 1"
|
|
|
+ src="./image/icon_loading.png"
|
|
|
+ alt=""
|
|
|
+ />
|
|
|
+ <span v-if="!msg.isAnswer">{{ msg.content }}</span>
|
|
|
+ <div v-else>
|
|
|
+ <div v-html="renderedMessage(msg.content)" class="markdown-body"></div>
|
|
|
+ <LoadingDots
|
|
|
+ v-if="index === messages.length - 1 && msg.isAnswer && loadingAnswer"
|
|
|
+ ></LoadingDots>
|
|
|
+ <div></div>
|
|
|
+ </div>
|
|
|
+ <!-- 评价 -->
|
|
|
+ <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 > 29 && queryTime < 120"
|
|
|
+ @click="handlePause"
|
|
|
+ >
|
|
|
+ <div class="dot"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- 滚动到底部icon -->
|
|
|
+ <div v-if="showScrollButton" class="scroll-to-bottom-btn" @click="scrollToBottom">
|
|
|
+ <span class="font_family icon-icon_movedown_b"></span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <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>
|
|
|
+</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 {
|
|
|
+ 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;
|
|
|
+ font-weight: 700;
|
|
|
+ }
|
|
|
+ .option-icon {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+ .font_family {
|
|
|
+ font-size: 16px;
|
|
|
+ cursor: pointer;
|
|
|
+ &:hover {
|
|
|
+ color: var(--color-theme);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .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 18px;
|
|
|
+ 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: 5px;
|
|
|
+
|
|
|
+ button.el-button + .el-button {
|
|
|
+ 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 {
|
|
|
+ position: absolute;
|
|
|
+ top: -2px;
|
|
|
+ color: #c9353f;
|
|
|
+ border-radius: 50%;
|
|
|
+ border: none;
|
|
|
+ }
|
|
|
+ .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: 4px;
|
|
|
+ animation: loading-rotate 2s linear infinite;
|
|
|
+ }
|
|
|
+ .markdown-body {
|
|
|
+ background: transparent;
|
|
|
+ }
|
|
|
+ .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 {
|
|
|
+ span {
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .scroll-to-bottom-btn {
|
|
|
+ position: absolute;
|
|
|
+ right: 50%;
|
|
|
+ transform: translateX(50%);
|
|
|
+ bottom: 58px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ height: 32px;
|
|
|
+ width: 32px;
|
|
|
+ border-radius: 50%;
|
|
|
+ // background-color: #f5f4f4;
|
|
|
+ background: rgba(255, 255, 255, 0.6); /* 半透明背景色 */
|
|
|
+ box-shadow: 2px 2px 12px 0px rgba(0, 0, 0, 0.3);
|
|
|
+ backdrop-filter: blur(1px); /* 应用10px的模糊效果 */
|
|
|
+ span {
|
|
|
+ color: #2b2f36;
|
|
|
+ }
|
|
|
+ cursor: pointer;
|
|
|
+ &:hover {
|
|
|
+ background-color: #fceee3;
|
|
|
+ span {
|
|
|
+ color: var(--color-theme);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .footer-input {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-end;
|
|
|
+ gap: 12px;
|
|
|
+ padding: 4px 12px;
|
|
|
+ padding-right: 4px;
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes loading-rotate {
|
|
|
+ 0% {
|
|
|
+ transform: rotate(0deg);
|
|
|
+ }
|
|
|
+
|
|
|
+ 100% {
|
|
|
+ transform: rotate(360deg);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|