<template>
|
<div class="ai-chat-sidebar-wrapper">
|
<!-- 悬浮图标 -->
|
<div class="ai-chat-trigger" @click="toggleSidebar" v-show="!visible">
|
<el-tooltip :content="currentAssistant.tooltip" placement="left">
|
<div class="trigger-icon">
|
<el-icon :size="30" color="#fff"><component :is="currentAssistant.icon" /></el-icon>
|
</div>
|
</el-tooltip>
|
</div>
|
|
<!-- 侧边栏对话框 -->
|
<el-drawer
|
v-model="visible"
|
:size="drawerSize"
|
direction="rtl"
|
:with-header="true"
|
class="ai-chat-drawer"
|
:modal="false"
|
modal-class="ai-chat-overlay"
|
:show-close="false"
|
:append-to-body="false"
|
@close="handleClose"
|
>
|
<template #header>
|
<div class="drawer-header">
|
<div class="header-left">
|
<el-icon :size="20" class="header-icon"><component :is="currentAssistant.icon" /></el-icon>
|
<span class="title">{{ currentAssistant.title }}</span>
|
</div>
|
<div v-if="showAssistantSwitch" class="assistant-switcher">
|
<el-radio-group v-model="selectedAssistantKey" size="small">
|
<el-radio-button
|
v-for="assistant in assistants"
|
:key="assistant.key"
|
:label="assistant.key"
|
>
|
{{ assistant.label }}
|
</el-radio-button>
|
</el-radio-group>
|
</div>
|
<div class="header-actions">
|
<el-tooltip content="会话历史" placement="bottom">
|
<el-button link class="header-action-btn" @click="handleToggleHistory">
|
<el-icon :size="18"><Timer /></el-icon>
|
</el-button>
|
</el-tooltip>
|
<el-tooltip content="开启新会话" placement="bottom">
|
<el-button link class="header-action-btn" @click="handleNewChat">
|
<el-icon :size="18"><Plus /></el-icon>
|
</el-button>
|
</el-tooltip>
|
<div class="action-divider"></div>
|
<el-tooltip content="关闭" placement="bottom">
|
<el-button link class="header-action-btn close-btn" @click="handleManualClose">
|
<el-icon :size="18"><Close /></el-icon>
|
</el-button>
|
</el-tooltip>
|
</div>
|
</div>
|
</template>
|
|
<div class="chat-container">
|
<!-- 历史会话列表 -->
|
<div v-if="showHistory" class="history-panel">
|
<div class="history-header">
|
<span>最近会话</span>
|
<el-button link type="primary" @click="showHistory = false">返回对话</el-button>
|
</div>
|
<el-skeleton :loading="loadingSessions" animated>
|
<template #template>
|
<div v-for="i in 5" :key="i" style="padding: 10px">
|
<el-skeleton-item variant="p" style="width: 80%" />
|
</div>
|
</template>
|
<div class="session-list">
|
<div
|
v-for="session in sessions"
|
:key="session.memoryId"
|
:class="['session-item', { active: uuid === session.memoryId }]"
|
@click="selectSession(session)"
|
>
|
<el-icon><ChatDotSquare /></el-icon>
|
<span class="session-name" :title="session.lastMessage || '新会话'">
|
{{ session.lastMessage || '新会话' }}
|
</span>
|
<el-button
|
link
|
type="danger"
|
class="delete-btn"
|
@click.stop="handleDeleteSession(session.memoryId)"
|
>
|
<el-icon><Delete /></el-icon>
|
</el-button>
|
</div>
|
<el-empty v-if="sessions.length === 0" :description="currentAssistant.emptySessionText" />
|
</div>
|
</el-skeleton>
|
</div>
|
|
<div v-else class="chat-main">
|
<div :class="['chat-hero', { compact: hasMessages }]">
|
<div :class="['assistant-stand', { thinking: isSending, compact: hasMessages }]">
|
<div class="assistant-halo"></div>
|
<div class="assistant-scan-ring"></div>
|
<div class="assistant-orbit assistant-orbit-a"></div>
|
<div class="assistant-orbit assistant-orbit-b"></div>
|
<div class="assistant-bot">
|
<div class="assistant-bot-antenna assistant-bot-antenna-left"></div>
|
<div class="assistant-bot-antenna assistant-bot-antenna-right"></div>
|
<div class="assistant-bot-head">
|
<div class="assistant-bot-head-glow"></div>
|
<div class="assistant-bot-eye assistant-bot-eye-left"></div>
|
<div class="assistant-bot-eye assistant-bot-eye-right"></div>
|
<div class="assistant-bot-mouth"></div>
|
</div>
|
<div class="assistant-bot-neck"></div>
|
<div class="assistant-bot-body">
|
<div class="assistant-bot-core">
|
<div class="assistant-bot-core-ring"></div>
|
<el-icon :size="22"><component :is="currentAssistant.icon" /></el-icon>
|
</div>
|
<div class="assistant-bot-arm assistant-bot-arm-left"></div>
|
<div class="assistant-bot-arm assistant-bot-arm-right"></div>
|
</div>
|
</div>
|
<div class="assistant-status">
|
<span class="assistant-status-dot"></span>
|
{{ isSending ? '思考中...' : currentAssistant.label }}
|
</div>
|
<div class="assistant-base assistant-base-lg"></div>
|
<div class="assistant-base assistant-base-md"></div>
|
<div class="assistant-base assistant-base-sm"></div>
|
</div>
|
|
<div :class="['welcome-card', { compact: hasMessages }]">
|
<div class="welcome-eyebrow">智能助手</div>
|
<h3 class="welcome-title">
|
您好
|
<br />
|
我是{{ currentAssistant.label }}分析解读助手
|
</h3>
|
<p class="welcome-desc">
|
{{ currentAssistant.description || '我可以围绕业务问题提供解读、查询建议和分析支持,帮助你更快完成判断与处理。' }}
|
</p>
|
|
<div class="quick-prompt-list">
|
<button
|
v-for="prompt in displayedQuickPrompts"
|
:key="prompt"
|
type="button"
|
class="quick-prompt-btn"
|
:disabled="isSending"
|
@click="sendQuickPrompt(prompt)"
|
>
|
{{ prompt }}
|
</button>
|
</div>
|
|
<button
|
v-if="quickPrompts.length > quickPromptLimit"
|
type="button"
|
class="more-prompts-btn"
|
@click="refreshQuickPrompts"
|
>
|
<el-icon><RefreshRight /></el-icon>
|
<span>换一换</span>
|
</button>
|
</div>
|
</div>
|
|
<div v-show="!hasMessages" class="hero-dot-grid" aria-hidden="true">
|
<span v-for="dot in 28" :key="dot"></span>
|
</div>
|
|
<div class="message-list" ref="messageListRef">
|
<div
|
v-for="(message, index) in messages"
|
:key="index"
|
:class="['message-item', message.isUser ? 'user-message' : 'bot-message']"
|
>
|
<div class="avatar">
|
<el-icon v-if="message.isUser"><User /></el-icon>
|
<el-icon v-else><Cpu /></el-icon>
|
</div>
|
<div class="message-content">
|
<!-- 文本内容 -->
|
<div class="text-box" v-html="message.htmlContent"></div>
|
|
<!-- 图表内容 -->
|
<div v-if="message.chartOptions && message.chartRenderReady" class="charts-wrapper">
|
<div
|
v-for="(option, key) in message.chartOptions"
|
:key="key"
|
class="chart-item"
|
:id="`ai-chart-${index}-${key}`"
|
></div>
|
</div>
|
|
<!-- 表格内容 -->
|
<div v-if="message.type === 'todo_list' && message.tableData" class="table-wrapper">
|
<el-table :data="message.tableData.items" border stripe size="small" style="width: 100%">
|
<el-table-column
|
v-for="col in message.tableData.columns"
|
:key="col"
|
:prop="col"
|
:label="columnLabelMap[col] || col"
|
min-width="100"
|
show-overflow-tooltip
|
/>
|
</el-table>
|
</div>
|
|
<!-- 打字中动画 -->
|
<div v-if="message.isTyping" class="typing-indicator">
|
<span class="dot"></span>
|
<span class="dot"></span>
|
<span class="dot"></span>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<div class="input-area">
|
<div class="input-actions">
|
<el-button link class="utility-action-btn" type="primary" size="small" @click="handleNewChat">
|
<el-icon><Plus /></el-icon>新会话
|
</el-button>
|
<el-button v-if="isSending" link class="utility-action-btn stop-action-btn" type="danger" size="small" @click="stopGeneration">
|
<el-icon><VideoPause /></el-icon>停止生成
|
</el-button>
|
<el-upload
|
v-if="currentAssistant.allowFileUpload"
|
class="file-upload-trigger"
|
action="#"
|
:auto-upload="false"
|
:show-file-list="false"
|
:on-change="handleFileChange"
|
:disabled="isSending"
|
>
|
<el-button link class="utility-action-btn upload-action-btn" type="primary" size="small" :disabled="isSending">
|
<el-icon><Upload /></el-icon>分析文件
|
</el-button>
|
</el-upload>
|
</div>
|
<div class="input-box">
|
<div v-if="selectedFile" class="selected-file-tag">
|
<el-icon><Document /></el-icon>
|
<span class="file-name">{{ selectedFile.name }}</span>
|
<el-icon class="remove-file" @click="removeSelectedFile"><Close /></el-icon>
|
</div>
|
<el-input
|
v-model="inputMessage"
|
type="textarea"
|
:rows="selectedFile ? 2 : 3"
|
:placeholder="currentAssistant.placeholder"
|
resize="none"
|
@keydown.enter.exact.prevent="sendMessage"
|
/>
|
<el-button
|
type="primary"
|
class="send-btn"
|
:disabled="isSending || (!inputMessage.trim() && !selectedFile)"
|
@click="sendMessage"
|
aria-label="发送"
|
>
|
<el-icon><Promotion /></el-icon>
|
</el-button>
|
</div>
|
</div>
|
</div>
|
</div>
|
</el-drawer>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, onMounted, onUnmounted, nextTick, watch, computed } from 'vue'
|
import request from '@/utils/request'
|
import * as echarts from 'echarts'
|
import { Cpu, User, Plus, Timer, Delete, ChatDotSquare, VideoPause, Upload, Document, Close, ShoppingCart, Promotion, RefreshRight } from '@element-plus/icons-vue'
|
import { ElMessage } from 'element-plus'
|
|
const props = defineProps({
|
assistants: {
|
type: Array,
|
default: () => []
|
},
|
defaultAssistant: {
|
type: String,
|
default: ''
|
}
|
})
|
|
const builtInAssistants = [
|
{
|
key: 'general',
|
label: '待办助理',
|
title: '待办智能助理',
|
tooltip: '待办助手',
|
icon: Cpu,
|
apiBase: '/xiaozhi',
|
storageKey: 'ai_chat_uuid',
|
placeholder: '请输入您的问题... (Enter 发送, Shift+Enter 换行)',
|
welcomeMessage: '你好',
|
description: '我可以回答你的问题,为你提供业务数据解读信息、处理建议和辅助决策支持。',
|
allowFileUpload: true,
|
emptySessionText: '暂无历史会话'
|
},
|
{
|
key: 'purchase',
|
label: '采购助理',
|
title: '采购智能助理',
|
tooltip: '采购智能助理',
|
icon: ShoppingCart,
|
apiBase: '/purchase-ai',
|
storageKey: 'purchase_ai_chat_uuid',
|
placeholder: '请输入采购问题... (Enter 发送, Shift+Enter 换行)',
|
welcomeMessage: '你好',
|
description: '我可以协助你分析采购订单、到货进度、供应商表现和付款情况,帮助你快速定位采购异常。',
|
allowFileUpload: false,
|
emptySessionText: '暂无采购会话'
|
}
|
]
|
|
const assistants = computed(() => props.assistants?.length ? props.assistants : builtInAssistants)
|
const selectedAssistantKey = ref(props.defaultAssistant || assistants.value[0]?.key || 'general')
|
const currentAssistant = computed(() => assistants.value.find(item => item.key === selectedAssistantKey.value) || assistants.value[0] || builtInAssistants[0])
|
const showAssistantSwitch = computed(() => assistants.value.length > 1)
|
const assistantQuickPromptMap = {
|
general: [
|
'我当前有哪些审批待办需要处理?',
|
'帮我列出今天新增的审批待办。',
|
'当前待我审批的单据,按时间倒序列出来。',
|
'我发起的审批里,哪些还在处理中?',
|
'查询流程编号 XXX 的审批详情。',
|
'流程编号 XXX 现在卡在哪个审批节点?当前审批人是谁?',
|
'帮我查看流程编号 XXX 的审批流转记录。',
|
'近7天我的审批待办统计情况怎么样?',
|
'本月我的审批中,通过、驳回、处理中各有多少?',
|
'近30天各类型审批数量分布是什么?',
|
'帮我审批通过流程编号 XXX,备注“同意”。',
|
'帮我驳回流程编号 XXX,备注“请补充说明”。',
|
'撤销我刚刚对流程编号 XXX 的审批操作。',
|
'帮我修改流程编号 XXX 的备注为“已补充附件”。',
|
'删除我发起的流程编号 XXX。'
|
],
|
purchase: [
|
'本月采购金额排名前十的物料有哪些?',
|
'哪些采购订单还未入库?',
|
'最近7天供应商到货异常有哪些?',
|
'帮我统计待付款采购单',
|
'列出本月采购退货情况'
|
]
|
}
|
const quickPromptLimit = 3
|
const quickPromptStart = ref(0)
|
const quickPrompts = computed(() => {
|
const assistant = currentAssistant.value || {}
|
if (Array.isArray(assistant.quickPrompts) && assistant.quickPrompts.length) {
|
return assistant.quickPrompts
|
}
|
return assistantQuickPromptMap[assistant.key] || assistantQuickPromptMap.general
|
})
|
const displayedQuickPrompts = computed(() => {
|
const prompts = quickPrompts.value || []
|
if (prompts.length <= quickPromptLimit) return prompts
|
|
const result = []
|
for (let i = 0; i < quickPromptLimit; i++) {
|
result.push(prompts[(quickPromptStart.value + i) % prompts.length])
|
}
|
return result
|
})
|
const hasMessages = computed(() => messages.value.length > 0)
|
|
const visible = ref(false)
|
const windowWidth = ref(window.innerWidth)
|
const drawerSize = computed(() => {
|
if (windowWidth.value < 768) return '100%'
|
if (windowWidth.value < 1200) return '500px'
|
return '600px'
|
})
|
const messageListRef = ref(null)
|
const isSending = ref(false)
|
const currentAbortController = ref(null)
|
const inputMessage = ref('')
|
const selectedFile = ref(null)
|
const messages = ref([])
|
const uuid = ref('')
|
const chartInstances = ref({})
|
const resizeHandlers = ref([])
|
const outputState = ref({})
|
|
// 历史会话相关
|
const showHistory = ref(false)
|
const sessions = ref([])
|
const loadingSessions = ref(false)
|
|
const abortCurrentRequest = () => {
|
if (!currentAbortController.value) return
|
|
currentAbortController.value.abort()
|
currentAbortController.value = null
|
isSending.value = false
|
|
const lastMsg = messages.value[messages.value.length - 1]
|
if (lastMsg && !lastMsg.isUser) {
|
lastMsg.isTyping = false
|
}
|
}
|
|
const toggleHistory = () => {
|
showHistory.value = !showHistory.value
|
if (showHistory.value) {
|
loadSessions()
|
}
|
}
|
|
const handleToggleHistory = () => {
|
if (isSending.value) {
|
abortCurrentRequest()
|
}
|
toggleHistory()
|
}
|
|
const loadSessions = async () => {
|
loadingSessions.value = true
|
try {
|
const res = await request.get(`${currentAssistant.value.apiBase}/history/sessions`)
|
if (res.code === 200) {
|
sessions.value = res.data || []
|
}
|
} catch (err) {
|
console.error('Failed to load sessions', err)
|
} finally {
|
loadingSessions.value = false
|
}
|
}
|
|
const selectSession = async (session) => {
|
showHistory.value = false
|
uuid.value = session.memoryId
|
localStorage.setItem(currentAssistant.value.storageKey, uuid.value)
|
|
// 加载会话消息
|
try {
|
const res = await request.get(`${currentAssistant.value.apiBase}/history/messages/${uuid.value}`)
|
if (res.code === 200) {
|
disposeCharts()
|
messages.value = []
|
const historyMsgs = res.data || []
|
|
// 重新构造消息列表并解析
|
historyMsgs.forEach((msg, idx) => {
|
const isUser = msg.role === 'user'
|
const botMsgIndex = messages.value.length
|
|
const messageObj = {
|
isUser,
|
content: msg.content,
|
htmlContent: '',
|
isTyping: false,
|
chartOptions: null,
|
chartRenderReady: false,
|
type: '',
|
tableData: null
|
}
|
|
messages.value.push(messageObj)
|
|
if (!isUser) {
|
outputState.value[botMsgIndex] = {
|
isPaused: false,
|
jsonBlockStartPos: -1,
|
jsBlockStartPos: -1,
|
blockEndPos: -1,
|
hasRenderedChart: false
|
}
|
|
// 解析历史消息中的 JSON
|
const extracted = extractEmbeddedSuccessJson(msg.content)
|
if (extracted) {
|
applyStructuredMessageData(messageObj, extracted.data, botMsgIndex)
|
}
|
|
updateOutputState(msg.content, botMsgIndex)
|
messageObj.htmlContent = convertStreamOutput(msg.content, botMsgIndex)
|
} else {
|
messageObj.htmlContent = convertTextToHtml(msg.content)
|
}
|
})
|
scrollToBottom()
|
}
|
} catch (err) {
|
console.error('Failed to load messages', err)
|
}
|
}
|
|
const handleDeleteSession = async (memoryId) => {
|
try {
|
const res = await request.delete(`${currentAssistant.value.apiBase}/history/${memoryId}`)
|
if (res.code === 200) {
|
loadSessions()
|
if (uuid.value === memoryId) {
|
newChat()
|
}
|
}
|
} catch (err) {
|
console.error('Failed to delete session', err)
|
}
|
}
|
|
const columnLabelMap = {
|
approveId: '审批编号',
|
approveType: '审批类型',
|
approveUserName: '审批人',
|
approveUserCurrentName: '当前处理人',
|
approveReason: '审批原因',
|
approveStatus: '审批状态',
|
createTime: '创建时间'
|
}
|
|
onMounted(() => {
|
initUUID()
|
// 初始欢迎
|
if (messages.value.length === 0) {
|
hello()
|
}
|
window.addEventListener('resize', handleWindowResize)
|
})
|
|
onUnmounted(() => {
|
disposeCharts()
|
window.removeEventListener('resize', handleWindowResize)
|
})
|
|
watch(selectedAssistantKey, (nextKey, prevKey) => {
|
if (!prevKey || nextKey === prevKey) return
|
|
abortCurrentRequest()
|
disposeCharts()
|
messages.value = []
|
outputState.value = {}
|
sessions.value = []
|
showHistory.value = false
|
selectedFile.value = null
|
inputMessage.value = ''
|
quickPromptStart.value = 0
|
initUUID()
|
hello()
|
})
|
|
const handleWindowResize = () => {
|
windowWidth.value = window.innerWidth
|
}
|
|
const toggleSidebar = () => {
|
visible.value = !visible.value
|
if (visible.value) {
|
scrollToBottom()
|
}
|
}
|
|
const handleClose = () => {
|
visible.value = false
|
}
|
|
const handleManualClose = () => {
|
if (isSending.value) {
|
abortCurrentRequest()
|
}
|
handleClose()
|
}
|
|
const initUUID = () => {
|
let storedUUID = localStorage.getItem(currentAssistant.value.storageKey)
|
if (!storedUUID) {
|
storedUUID = Math.random().toString(36).substring(2, 10) + Date.now().toString(36).substring(4)
|
localStorage.setItem(currentAssistant.value.storageKey, storedUUID)
|
}
|
uuid.value = storedUUID
|
}
|
|
const hello = () => {
|
sendRequest(currentAssistant.value.welcomeMessage || '你好')
|
}
|
|
const newChat = () => {
|
disposeCharts()
|
messages.value = []
|
outputState.value = {}
|
sessions.value = []
|
showHistory.value = false
|
selectedFile.value = null
|
quickPromptStart.value = 0
|
localStorage.removeItem(currentAssistant.value.storageKey)
|
initUUID()
|
hello()
|
}
|
|
const handleNewChat = () => {
|
if (isSending.value) {
|
abortCurrentRequest()
|
}
|
newChat()
|
}
|
|
const sendQuickPrompt = (prompt) => {
|
if (!prompt || isSending.value) return
|
inputMessage.value = prompt
|
sendMessage()
|
}
|
|
const refreshQuickPrompts = () => {
|
const prompts = quickPrompts.value || []
|
if (prompts.length <= quickPromptLimit) return
|
quickPromptStart.value = (quickPromptStart.value + quickPromptLimit) % prompts.length
|
}
|
|
const disposeCharts = () => {
|
Object.values(chartInstances.value).forEach(chart => chart.dispose())
|
resizeHandlers.value.forEach(handler => window.removeEventListener('resize', handler))
|
chartInstances.value = {}
|
resizeHandlers.value = []
|
}
|
|
const extractEmbeddedSuccessJson = (text) => {
|
if (!text || typeof text !== 'string') return null
|
|
const startIdx = text.indexOf('{"success"')
|
if (startIdx === -1) return null
|
|
for (let i = startIdx; i < text.length; i++) {
|
if (text[i] !== '{') continue
|
|
let depth = 0
|
let inString = false
|
let escaped = false
|
|
for (let j = i; j < text.length; j++) {
|
const char = text[j]
|
|
if (inString) {
|
if (escaped) {
|
escaped = false
|
} else if (char === '\\') {
|
escaped = true
|
} else if (char === '"') {
|
inString = false
|
}
|
continue
|
}
|
|
if (char === '"') {
|
inString = true
|
continue
|
}
|
|
if (char === '{') {
|
depth++
|
} else if (char === '}') {
|
depth--
|
if (depth === 0) {
|
const candidate = text.slice(i, j + 1)
|
try {
|
const parsed = JSON.parse(candidate)
|
if (parsed?.success === true) {
|
return {
|
data: parsed,
|
startIdx: i,
|
endIdx: j + 1
|
}
|
}
|
} catch (err) {
|
continue
|
}
|
}
|
}
|
}
|
}
|
|
return null
|
}
|
|
const applyStructuredMessageData = (messageObj, parsedData, msgIndex, shouldRenderCharts = true) => {
|
if (!messageObj || !parsedData?.success) return
|
|
messageObj.type = parsedData.type || ''
|
|
if (messageObj.type === 'todo_list' && parsedData.data) {
|
messageObj.tableData = parsedData.data
|
}
|
|
if (parsedData.charts && Object.keys(parsedData.charts).length > 0) {
|
messageObj.chartOptions = parsedData.charts
|
messageObj.chartRenderReady = true
|
|
if (shouldRenderCharts) {
|
renderCharts(msgIndex, messageObj.chartOptions)
|
if (outputState.value[msgIndex]) {
|
outputState.value[msgIndex].hasRenderedChart = true
|
}
|
}
|
}
|
}
|
|
const scrollToBottom = () => {
|
nextTick(() => {
|
if (messageListRef.value) {
|
messageListRef.value.scrollTop = messageListRef.value.scrollHeight
|
}
|
})
|
}
|
|
const handleFileChange = (file) => {
|
if (!file) return
|
const rawFile = file.raw
|
if (rawFile) {
|
// 限制文件大小,例如 10MB
|
const isLt10M = rawFile.size / 1024 / 1024 < 10
|
if (!isLt10M) {
|
ElMessage.error('文件大小不能超过 10MB!')
|
return
|
}
|
selectedFile.value = rawFile
|
}
|
}
|
|
const removeSelectedFile = () => {
|
selectedFile.value = null
|
}
|
|
const analyzeFile = async (file, message = '') => {
|
if (isSending.value) return
|
isSending.value = true
|
currentAbortController.value = new AbortController()
|
|
const userMsg = message ? `${message}\n[上传文件分析] ${file.name}` : `[上传文件分析] ${file.name}`
|
messages.value.push({
|
isUser: true,
|
content: userMsg,
|
htmlContent: convertTextToHtml(userMsg),
|
isTyping: false
|
})
|
|
const botMsgIndex = messages.value.length
|
messages.value.push({
|
isUser: false,
|
content: '',
|
htmlContent: '',
|
isTyping: true,
|
chartOptions: null,
|
chartRenderReady: false,
|
type: '',
|
tableData: null
|
})
|
|
outputState.value[botMsgIndex] = {
|
isPaused: false,
|
jsonBlockStartPos: -1,
|
jsBlockStartPos: -1,
|
blockEndPos: -1,
|
hasRenderedChart: false
|
}
|
|
scrollToBottom()
|
|
const formData = new FormData()
|
formData.append('file', file)
|
formData.append('memoryId', uuid.value)
|
if (message.trim()) {
|
formData.append('message', message.trim())
|
}
|
|
request.post(`${currentAssistant.value.apiBase}/analyze-file`, formData, {
|
headers: {
|
'Content-Type': 'multipart/form-data'
|
},
|
signal: currentAbortController.value.signal,
|
onDownloadProgress: (e) => {
|
const fullText = e.target ? e.target.responseText : (e.event ? e.event.target.responseText : '')
|
if (!fullText) return
|
|
const currentMsg = messages.value[botMsgIndex]
|
if (!currentMsg) return
|
|
currentMsg.content = fullText
|
|
// 解析 JSON 数据(针对嵌入式 JSON)
|
const extracted = extractEmbeddedSuccessJson(fullText)
|
if (extracted) {
|
applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex, !outputState.value[botMsgIndex].hasRenderedChart)
|
}
|
|
updateOutputState(fullText, botMsgIndex)
|
currentMsg.htmlContent = convertStreamOutput(fullText, botMsgIndex)
|
scrollToBottom()
|
}
|
}).then(() => {
|
const currentMsg = messages.value[botMsgIndex]
|
currentMsg.isTyping = false
|
isSending.value = false
|
currentAbortController.value = null
|
|
// 最终解析确保图表渲染
|
if (currentMsg.chartOptions && !outputState.value[botMsgIndex].hasRenderedChart) {
|
renderCharts(botMsgIndex, currentMsg.chartOptions)
|
outputState.value[botMsgIndex].hasRenderedChart = true
|
}
|
}).catch(err => {
|
if (err.name === 'CanceledError' || err.name === 'AbortError') {
|
console.log('Analysis aborted by user')
|
return
|
}
|
console.error('File analysis error:', err)
|
const errorMsg = '抱歉,文件分析过程中遇到了一点问题,请稍后再试。'
|
if (messages.value[botMsgIndex]) {
|
messages.value[botMsgIndex].content = errorMsg
|
messages.value[botMsgIndex].htmlContent = convertTextToHtml(errorMsg)
|
messages.value[botMsgIndex].isTyping = false
|
}
|
isSending.value = false
|
currentAbortController.value = null
|
})
|
}
|
|
const sendMessage = () => {
|
const msg = inputMessage.value?.trim() || ''
|
if ((msg || selectedFile.value) && !isSending.value) {
|
if (selectedFile.value) {
|
analyzeFile(selectedFile.value, msg)
|
selectedFile.value = null
|
} else {
|
sendRequest(msg)
|
}
|
inputMessage.value = ''
|
}
|
}
|
|
const stopGeneration = () => {
|
abortCurrentRequest()
|
}
|
|
const sendRequest = (message) => {
|
isSending.value = true
|
currentAbortController.value = new AbortController()
|
|
// 用户消息
|
messages.value.push({
|
isUser: true,
|
content: message,
|
htmlContent: convertTextToHtml(message),
|
isTyping: false
|
})
|
|
// 机器人占位
|
const botMsgIndex = messages.value.length
|
const botMsg = {
|
isUser: false,
|
content: '',
|
htmlContent: '',
|
isTyping: true,
|
chartOptions: null,
|
chartRenderReady: false,
|
type: '',
|
tableData: null
|
}
|
messages.value.push(botMsg)
|
|
outputState.value[botMsgIndex] = {
|
isPaused: false,
|
jsonBlockStartPos: -1,
|
jsBlockStartPos: -1,
|
blockEndPos: -1,
|
hasRenderedChart: false
|
}
|
|
scrollToBottom()
|
|
request.post(`${currentAssistant.value.apiBase}/chat`,
|
{ memoryId: uuid.value, message },
|
{
|
signal: currentAbortController.value.signal,
|
onDownloadProgress: (e) => {
|
// 兼容不同版本的 axios 获取响应文本的方式
|
const fullText = e.target ? e.target.responseText : (e.event ? e.event.target.responseText : '')
|
if (!fullText) return
|
|
const currentMsg = messages.value[botMsgIndex]
|
if (!currentMsg) return
|
|
currentMsg.content = fullText
|
|
// 尝试提取并解析 JSON
|
const extracted = extractEmbeddedSuccessJson(fullText)
|
if (extracted) {
|
applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex)
|
} else {
|
const extractJson = (text) => {
|
const startIdx = text.indexOf('{"success": true')
|
if (startIdx === -1) return null
|
|
// 从后往前找最后一个 '}'
|
const lastBraceIdx = text.lastIndexOf('}')
|
if (lastBraceIdx === -1 || lastBraceIdx < startIdx) return null
|
|
const potentialJson = text.substring(startIdx, lastBraceIdx + 1)
|
try {
|
return JSON.parse(potentialJson)
|
} catch (err) {
|
return null
|
}
|
}
|
|
const parsedData = extractJson(fullText)
|
if (parsedData) {
|
currentMsg.type = parsedData.type || ''
|
if (currentMsg.type === 'todo_list' && parsedData.data) {
|
currentMsg.tableData = parsedData.data
|
}
|
if (parsedData.charts && Object.keys(parsedData.charts).length > 0) {
|
currentMsg.chartOptions = parsedData.charts
|
currentMsg.chartRenderReady = true
|
// 每次解析成功都尝试渲染/更新图表,以支持流式更新
|
renderCharts(botMsgIndex, currentMsg.chartOptions)
|
}
|
}
|
|
}
|
|
updateOutputState(fullText, botMsgIndex)
|
currentMsg.htmlContent = convertStreamOutput(fullText, botMsgIndex)
|
scrollToBottom()
|
}
|
}
|
).then(() => {
|
const currentMsg = messages.value[botMsgIndex]
|
currentMsg.isTyping = false
|
isSending.value = false
|
currentAbortController.value = null
|
|
// 最终解析
|
const extracted = extractEmbeddedSuccessJson(currentMsg.content)
|
if (extracted) {
|
applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex)
|
} else {
|
const extractJson = (text) => {
|
const startIdx = text.indexOf('{"success": true')
|
if (startIdx === -1) return null
|
const lastBraceIdx = text.lastIndexOf('}')
|
if (lastBraceIdx === -1 || lastBraceIdx < startIdx) return null
|
const potentialJson = text.substring(startIdx, lastBraceIdx + 1)
|
try {
|
return JSON.parse(potentialJson)
|
} catch (err) {
|
return null
|
}
|
}
|
|
const finalParsed = extractJson(currentMsg.content)
|
if (finalParsed) {
|
currentMsg.type = finalParsed.type || ''
|
if (currentMsg.type === 'todo_list' && finalParsed.data) {
|
currentMsg.tableData = finalParsed.data
|
}
|
if (finalParsed.charts && Object.keys(finalParsed.charts).length > 0) {
|
currentMsg.chartOptions = finalParsed.charts
|
currentMsg.chartRenderReady = true
|
renderCharts(botMsgIndex, currentMsg.chartOptions)
|
}
|
}
|
}
|
}).catch(err => {
|
if (err.name === 'CanceledError' || err.name === 'AbortError') {
|
console.log('Request aborted by user')
|
return
|
}
|
console.error('AI Chat Error:', err)
|
const errorMsg = '抱歉,我现在遇到了一点问题,请稍后再试。'
|
if (messages.value[botMsgIndex]) {
|
messages.value[botMsgIndex].content = errorMsg
|
messages.value[botMsgIndex].htmlContent = convertTextToHtml(errorMsg)
|
messages.value[botMsgIndex].isTyping = false
|
}
|
isSending.value = false
|
currentAbortController.value = null
|
})
|
}
|
|
const updateOutputState = (text, msgIndex) => {
|
const state = outputState.value[msgIndex]
|
if (state.jsonBlockStartPos === -1) {
|
const pos = text.indexOf('```json')
|
if (pos !== -1) { state.jsonBlockStartPos = pos; state.isPaused = true }
|
}
|
if (state.jsBlockStartPos === -1) {
|
const pos = text.indexOf('```javascript') !== -1 ? text.indexOf('```javascript') : text.indexOf('```js')
|
if (pos !== -1) { state.jsBlockStartPos = pos; state.isPaused = true }
|
}
|
if ((state.jsonBlockStartPos !== -1 || state.jsBlockStartPos !== -1) && state.blockEndPos === -1) {
|
const startCheck = state.jsonBlockStartPos !== -1 ? state.jsonBlockStartPos + 7 : state.jsBlockStartPos + (text.includes('javascript') ? 13 : 5)
|
const endPos = text.indexOf('```', startCheck)
|
if (endPos !== -1) { state.blockEndPos = endPos + 3; state.isPaused = false }
|
}
|
}
|
|
const convertTextToHtml = (text) => {
|
if (!text) return ''
|
return text
|
.replace(/&/g, '&')
|
.replace(/</g, '<')
|
.replace(/>/g, '>')
|
.replace(/\n/g, '<br>')
|
}
|
|
const convertStreamOutput = (output, msgIndex) => {
|
if (!output) return ''
|
const state = outputState.value[msgIndex]
|
let display = output
|
|
// 尝试提取 JSON 部分
|
const extracted = extractEmbeddedSuccessJson(output)
|
const startIdx = extracted ? extracted.startIdx : output.indexOf('{"success"')
|
|
// 如果还在代码块中且未结束,显示提示文字
|
if (state && ((state.jsonBlockStartPos !== -1) || (state.jsBlockStartPos !== -1)) && state.blockEndPos === -1) {
|
const startPos = state.jsonBlockStartPos !== -1 ? state.jsonBlockStartPos : state.jsBlockStartPos
|
const textBeforeBlock = display.substring(0, startPos).trim()
|
display = textBeforeBlock || '正在分析数据并生成图表...'
|
return convertTextToHtml(display)
|
}
|
|
if (extracted) {
|
const parsed = extracted.data
|
display = (output.substring(0, extracted.startIdx) + output.substring(extracted.endIdx)).trim()
|
|
if (/^[\s}\],,。.::;;]+$/.test(display)) {
|
display = ''
|
}
|
|
if (parsed.description) {
|
display = parsed.description
|
}
|
|
if (!display) {
|
if (parsed.type === 'todo_list') {
|
display = '已为您整理好相关数据。'
|
} else if (parsed.charts && Object.keys(parsed.charts).length > 0) {
|
display = '已为您生成分析图表。'
|
} else {
|
display = '正在为您展示分析结果...'
|
}
|
}
|
} else if (startIdx !== -1) {
|
const lastBraceIdx = output.lastIndexOf('}')
|
if (lastBraceIdx !== -1 && lastBraceIdx > startIdx) {
|
const potentialJson = output.substring(startIdx, lastBraceIdx + 1)
|
try {
|
const parsed = JSON.parse(potentialJson)
|
// 成功解析,移除 JSON 部分显示文字
|
display = (output.substring(0, startIdx) + output.substring(lastBraceIdx + 1)).trim()
|
|
if (/^[\s}\],,。.::;;]+$/.test(display)) {
|
display = ''
|
}
|
|
if (parsed.description) {
|
display = parsed.description
|
}
|
|
if (!display) {
|
if (parsed.type === 'todo_list') {
|
display = '已为您整理好相关数据:'
|
} else if (parsed.charts && Object.keys(parsed.charts).length > 0) {
|
display = '已为您生成分析图表:'
|
} else {
|
display = '正在为您展示分析结果...'
|
}
|
}
|
} catch (e) {
|
// 解析失败,说明 JSON 还在传输中或格式不正确
|
display = output.substring(0, startIdx).trim() || '正在分析数据并生成图表...'
|
}
|
} else {
|
// 找到了开始但还没找到结束
|
display = output.substring(0, startIdx).trim() || '正在分析数据并生成图表...'
|
}
|
}
|
|
let html = convertTextToHtml(display)
|
|
// 还原代码块
|
html = html.replace(/```(javascript|js)([\s\S]*?)```/g, '<pre class="code-block js-code">$2</pre>')
|
html = html.replace(/```([\s\S]*?)```/g, '<pre class="code-block">$1</pre>')
|
|
return html || '...'
|
}
|
|
const renderCharts = (msgIndex, chartOptions) => {
|
nextTick(() => {
|
Object.keys(chartOptions).forEach(key => {
|
const id = `ai-chart-${msgIndex}-${key}`
|
const tryInit = (count = 0) => {
|
const dom = document.getElementById(id)
|
if (dom) {
|
if (chartInstances.value[id]) {
|
// 如果已经初始化过,直接更新数据
|
const chart = chartInstances.value[id]
|
const option = normalizeAiChartOption(chartOptions[key])
|
if (option) chart.setOption(option)
|
return
|
}
|
|
const chart = echarts.init(dom)
|
chartInstances.value[id] = chart
|
const option = normalizeAiChartOption(chartOptions[key])
|
if (option) {
|
chart.setOption(option)
|
} else {
|
console.warn('Invalid chart option for:', id, chartOptions[key])
|
}
|
|
const handler = () => chart.resize()
|
resizeHandlers.value.push(handler)
|
window.addEventListener('resize', handler)
|
} else if (count < 15) { // 稍微增加重试次数
|
setTimeout(() => tryInit(count + 1), 200)
|
}
|
}
|
tryInit()
|
})
|
})
|
}
|
|
// 格式化 AI 返回的图表配置,将其转换为标准的 ECharts 配置
|
const formatChartOption = (rawOption) => {
|
if (!rawOption) return null
|
|
// 如果已经是标准 ECharts 配置(包含 series),则直接返回
|
const hasSeries = rawOption.series && Array.isArray(rawOption.series)
|
|
// 尝试转换简易格式
|
try {
|
const isPie = rawOption.type === 'pie' || (rawOption.title && rawOption.title.includes('占比'))
|
|
const option = {
|
title: {
|
text: rawOption.title || '',
|
left: 'center',
|
textStyle: { fontSize: 14 }
|
},
|
tooltip: {
|
trigger: isPie ? 'item' : 'axis'
|
},
|
legend: {
|
bottom: '0'
|
},
|
grid: {
|
left: '3%',
|
right: '4%',
|
bottom: '15%',
|
containLabel: true
|
},
|
xAxis: isPie ? undefined : {
|
type: 'category',
|
data: rawOption.xAxisData || (Array.isArray(rawOption.xAxis) ? rawOption.xAxis : []),
|
name: typeof rawOption.xAxis === 'string' ? rawOption.xAxis : ''
|
},
|
yAxis: isPie ? undefined : {
|
type: 'value',
|
name: typeof rawOption.yAxis === 'string' ? rawOption.yAxis : ''
|
},
|
series: rawOption.series || [{
|
name: rawOption.title || '数值',
|
type: rawOption.type || 'line',
|
data: rawOption.seriesData || (Array.isArray(rawOption.data) ? rawOption.data : []),
|
smooth: true,
|
radius: isPie ? '50%' : undefined
|
}]
|
}
|
|
// 针对饼图的特殊处理
|
if (isPie && !option.series[0].data && Array.isArray(rawOption.data)) {
|
option.series[0].data = rawOption.data
|
}
|
|
return option
|
} catch (err) {
|
console.error('Chart option conversion failed:', err)
|
return null
|
}
|
}
|
|
const normalizeAiChartOption = (rawOption) => {
|
if (!rawOption) return null
|
|
try {
|
const hasSeries = Array.isArray(rawOption.series) && rawOption.series.length > 0
|
const firstSeriesType = hasSeries ? rawOption.series[0]?.type : rawOption.type
|
const titleConfig = rawOption.title && typeof rawOption.title === 'object'
|
? rawOption.title
|
: null
|
const tooltipConfig = rawOption.tooltip && typeof rawOption.tooltip === 'object'
|
? rawOption.tooltip
|
: null
|
const legendConfig = rawOption.legend && typeof rawOption.legend === 'object'
|
? rawOption.legend
|
: null
|
const rawXAxisConfig = rawOption.xAxis && typeof rawOption.xAxis === 'object' && !Array.isArray(rawOption.xAxis)
|
? rawOption.xAxis
|
: null
|
const rawYAxisConfig = rawOption.yAxis && typeof rawOption.yAxis === 'object' && !Array.isArray(rawOption.yAxis)
|
? rawOption.yAxis
|
: null
|
const titleText = typeof rawOption.title === 'string' ? rawOption.title : rawOption.title?.text || ''
|
const isPie = firstSeriesType === 'pie' || titleText.includes('占比')
|
const baseXAxisData = Array.isArray(rawOption.xAxisData)
|
? rawOption.xAxisData
|
: (Array.isArray(rawOption.xAxis) ? rawOption.xAxis : (Array.isArray(rawXAxisConfig?.data) ? rawXAxisConfig.data : []))
|
const fallbackSeries = [{
|
name: titleText || '数据',
|
type: rawOption.type || 'line',
|
data: Array.isArray(rawOption.seriesData) ? rawOption.seriesData : (Array.isArray(rawOption.data) ? rawOption.data : [])
|
}]
|
const normalizedSeries = (hasSeries ? rawOption.series : fallbackSeries).map((seriesItem, index) => {
|
const seriesType = seriesItem?.type || rawOption.type || 'line'
|
const nextSeries = {
|
...seriesItem,
|
name: seriesItem?.name || titleText || `系列${index + 1}`,
|
type: seriesType
|
}
|
|
if (isPie) {
|
nextSeries.radius = nextSeries.radius || '55%'
|
nextSeries.data = Array.isArray(nextSeries.data) ? nextSeries.data : (Array.isArray(rawOption.data) ? rawOption.data : [])
|
} else {
|
nextSeries.smooth = typeof nextSeries.smooth === 'boolean' ? nextSeries.smooth : seriesType === 'line'
|
nextSeries.data = Array.isArray(nextSeries.data) ? nextSeries.data : []
|
}
|
|
return nextSeries
|
})
|
const categorySource = !isPie
|
? normalizedSeries.find(seriesItem => Array.isArray(seriesItem.data) && seriesItem.data.every(item => item && typeof item === 'object' && 'name' in item && 'value' in item))
|
: null
|
const xAxisData = categorySource
|
? categorySource.data.map(item => item.name)
|
: baseXAxisData
|
const finalSeries = !isPie && categorySource
|
? normalizedSeries.map(seriesItem => ({
|
...seriesItem,
|
data: Array.isArray(seriesItem.data)
|
? seriesItem.data.map(item => (item && typeof item === 'object' && 'value' in item ? item.value : item))
|
: []
|
}))
|
: normalizedSeries
|
|
return {
|
title: titleConfig || {
|
text: titleText,
|
left: 'center',
|
textStyle: { fontSize: 14 }
|
},
|
tooltip: tooltipConfig || {
|
trigger: isPie ? 'item' : 'axis'
|
},
|
legend: legendConfig || {
|
bottom: '0'
|
},
|
grid: isPie ? undefined : {
|
left: '3%',
|
right: '4%',
|
bottom: '15%',
|
containLabel: true
|
},
|
xAxis: isPie ? undefined : {
|
...(rawXAxisConfig || {}),
|
type: 'category',
|
data: xAxisData,
|
name: typeof rawOption.xAxis === 'string' ? rawOption.xAxis : (rawXAxisConfig?.name || '')
|
},
|
yAxis: isPie ? undefined : (rawYAxisConfig || {
|
type: 'value',
|
name: typeof rawOption.yAxis === 'string' ? rawOption.yAxis : ''
|
}),
|
series: finalSeries
|
}
|
} catch (err) {
|
console.error('AI chart normalization failed:', err, rawOption)
|
return formatChartOption(rawOption)
|
}
|
}
|
|
watch(messages, () => scrollToBottom(), { deep: true })
|
</script>
|
|
<style lang="scss">
|
.ai-chat-overlay {
|
pointer-events: none !important;
|
background: transparent !important;
|
}
|
|
.ai-chat-overlay .el-drawer {
|
pointer-events: auto;
|
}
|
</style>
|
|
<style scoped lang="scss">
|
$primary-blue: #0055d4;
|
$secondary-blue: #2e8ce0;
|
$light-blue: #7ab8ff;
|
$pale-blue: #c5dcff;
|
$ice-white: #e8f2ff;
|
$deep-blue: #003b8e;
|
$deepest-blue: #002b66;
|
$gradient-blue: linear-gradient(145deg, #004fc7 0%, #0066e0 40%, #2580e8 70%, #5a9fe0 100%);
|
$gradient-dark: linear-gradient(145deg, #003b8e 0%, #0055d4 50%, #0077e8 100%);
|
$gradient-ice: linear-gradient(180deg, #e0ecff 0%, #d4e5ff 50%, #e8f0ff 100%);
|
$shadow-blue: 0 8px 40px rgba(0, 85, 212, 0.35);
|
$shadow-deep: 0 12px 48px rgba(0, 40, 120, 0.4);
|
$shadow-card: 0 6px 24px rgba(0, 51, 136, 0.12);
|
|
.ai-chat-sidebar-wrapper {
|
position: static;
|
z-index: 2000;
|
pointer-events: auto;
|
|
:deep(.el-drawer) {
|
pointer-events: auto;
|
}
|
}
|
|
.ai-chat-trigger {
|
pointer-events: auto;
|
position: fixed;
|
right: 24px;
|
bottom: 100px;
|
width: 56px;
|
height: 56px;
|
background: $gradient-dark;
|
border-radius: 50%;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
cursor: pointer;
|
box-shadow: $shadow-deep, 0 0 0 2px rgba(0, 85, 212, 0.3) inset, 0 0 30px rgba(0, 119, 232, 0.2);
|
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
z-index: 2001;
|
animation: triggerPulse 3s ease-in-out infinite;
|
|
&::before {
|
content: '';
|
position: absolute;
|
inset: -6px;
|
background: linear-gradient(135deg, rgba(0, 85, 212, 0.4), rgba(0, 136, 232, 0.3), rgba(90, 159, 224, 0.2));
|
border-radius: 50%;
|
z-index: -1;
|
filter: blur(16px);
|
animation: glowPulse 2s ease-in-out infinite alternate;
|
}
|
|
&::after {
|
content: '';
|
position: absolute;
|
inset: 0;
|
border-radius: 50%;
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.3) 0%, transparent 50%);
|
pointer-events: none;
|
}
|
|
&:hover {
|
transform: scale(1.12) translateY(-4px);
|
box-shadow: $shadow-deep, 0 0 0 3px rgba(0, 136, 232, 0.4) inset, 0 0 50px rgba(0, 136, 232, 0.3);
|
|
&::before {
|
animation: glowPulse 1s ease-in-out infinite alternate;
|
}
|
|
.trigger-icon {
|
transform: rotate(-8deg) scale(1.05);
|
filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.5));
|
}
|
}
|
|
.trigger-icon {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
color: #fff;
|
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
|
}
|
}
|
|
@keyframes triggerPulse {
|
0%, 100% {
|
box-shadow: $shadow-blue, 0 0 0 2px rgba(0, 85, 212, 0.25) inset, 0 0 20px rgba(0, 119, 232, 0.15);
|
}
|
50% {
|
box-shadow: $shadow-deep, 0 0 0 3px rgba(0, 136, 232, 0.35) inset, 0 0 40px rgba(0, 136, 232, 0.25);
|
}
|
}
|
|
@keyframes glowPulse {
|
0% {
|
opacity: 0.6;
|
transform: scale(1);
|
}
|
100% {
|
opacity: 1;
|
transform: scale(1.1);
|
}
|
}
|
|
.ai-chat-drawer {
|
:deep(.el-drawer__body) {
|
padding: 0;
|
overflow: hidden;
|
display: flex;
|
flex-direction: column;
|
height: 100%;
|
}
|
:deep(.el-drawer__header) {
|
margin-bottom: 0;
|
padding: 0;
|
background: $gradient-dark;
|
color: #fff;
|
}
|
}
|
|
.drawer-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
width: 100%;
|
padding: 18px 20px;
|
background: $gradient-dark;
|
position: relative;
|
overflow: hidden;
|
|
&::before {
|
content: '';
|
position: absolute;
|
top: -60%;
|
right: -25%;
|
width: 250px;
|
height: 250px;
|
background: radial-gradient(circle, rgba(0, 136, 232, 0.4) 0%, transparent 70%);
|
pointer-events: none;
|
animation: headerGlow 4s ease-in-out infinite alternate;
|
}
|
|
&::after {
|
content: '';
|
position: absolute;
|
bottom: -40%;
|
left: -15%;
|
width: 200px;
|
height: 200px;
|
background: radial-gradient(circle, rgba(0, 85, 212, 0.3) 0%, transparent 70%);
|
pointer-events: none;
|
animation: headerGlow 5s ease-in-out infinite alternate-reverse;
|
}
|
|
.header-left {
|
display: flex;
|
align-items: center;
|
gap: 12px;
|
position: relative;
|
z-index: 1;
|
|
.header-icon {
|
color: rgba(255, 255, 255, 0.95);
|
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.2));
|
animation: iconFloat 3s ease-in-out infinite;
|
}
|
|
.title {
|
font-size: 17px;
|
font-weight: 600;
|
color: #fff;
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
letter-spacing: 0.5px;
|
}
|
}
|
|
.header-actions {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
position: relative;
|
z-index: 1;
|
|
.action-divider {
|
width: 1px;
|
height: 16px;
|
background: rgba(255, 255, 255, 0.2);
|
margin: 0 2px;
|
}
|
|
:deep(.el-button) {
|
color: rgba(255, 255, 255, 0.85);
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
background: rgba(255, 255, 255, 0.12);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
border-radius: 8px;
|
padding: 8px;
|
height: 32px;
|
width: 32px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
|
&:hover {
|
color: #fff;
|
background: rgba(255, 255, 255, 0.25);
|
border-color: rgba(255, 255, 255, 0.3);
|
transform: translateY(-1px);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
}
|
|
&:active {
|
transform: translateY(0);
|
}
|
|
&.close-btn {
|
background: rgba(255, 255, 255, 0.1);
|
&:hover {
|
background: rgba(245, 108, 108, 0.8);
|
border-color: rgba(245, 108, 108, 0.5);
|
}
|
}
|
}
|
|
:deep(.header-action-btn) {
|
position: relative;
|
overflow: hidden;
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.08));
|
border: 1px solid rgba(255, 255, 255, 0.16);
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14), 0 10px 18px rgba(0, 0, 0, 0.12);
|
|
&::before {
|
content: '';
|
position: absolute;
|
inset: 0;
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.22), transparent 55%);
|
pointer-events: none;
|
}
|
|
&::after {
|
content: '';
|
position: absolute;
|
top: -120%;
|
left: -40%;
|
width: 60%;
|
height: 260%;
|
background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.28), transparent);
|
transform: rotate(24deg);
|
opacity: 0;
|
transition: all 0.35s ease;
|
}
|
|
&:hover::after {
|
left: 100%;
|
opacity: 1;
|
}
|
}
|
}
|
|
.assistant-switcher {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
flex: 1;
|
padding: 0 12px;
|
position: relative;
|
z-index: 1;
|
|
:deep(.el-radio-group) {
|
display: flex;
|
gap: 6px;
|
flex-wrap: wrap;
|
justify-content: center;
|
padding: 4px;
|
border-radius: 999px;
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0.08));
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14), 0 10px 18px rgba(0, 0, 0, 0.1);
|
}
|
|
:deep(.el-radio-button__inner) {
|
border-radius: 999px;
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
background: rgba(255, 255, 255, 0.12);
|
color: rgba(255, 255, 255, 0.86);
|
box-shadow: none;
|
padding: 7px 14px;
|
font-weight: 500;
|
}
|
|
:deep(.el-radio-button:first-child .el-radio-button__inner),
|
:deep(.el-radio-button:last-child .el-radio-button__inner) {
|
border-radius: 999px;
|
}
|
|
:deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
|
background: #fff;
|
color: $primary-blue;
|
border-color: #fff;
|
box-shadow: 0 6px 14px rgba(0, 40, 120, 0.16);
|
}
|
}
|
}
|
|
@keyframes headerGlow {
|
0% {
|
opacity: 0.6;
|
transform: scale(1);
|
}
|
100% {
|
opacity: 1;
|
transform: scale(1.15);
|
}
|
}
|
|
@keyframes iconFloat {
|
0%, 100% {
|
transform: translateY(0) rotate(0);
|
}
|
50% {
|
transform: translateY(-2px) rotate(3deg);
|
}
|
}
|
|
.chat-container {
|
display: flex;
|
flex-direction: column;
|
height: 100%;
|
width: 100%;
|
background: $ice-white;
|
position: relative;
|
overflow: hidden;
|
|
&::before {
|
content: '';
|
position: absolute;
|
top: 0;
|
left: 0;
|
right: 0;
|
height: 240px;
|
background: linear-gradient(180deg, rgba(0, 85, 212, 0.06) 0%, transparent 100%);
|
pointer-events: none;
|
}
|
}
|
|
.history-panel {
|
position: absolute;
|
inset: 0;
|
background: linear-gradient(180deg, #fff 0%, $ice-white 100%);
|
z-index: 10;
|
display: flex;
|
flex-direction: column;
|
box-shadow: -8px 0 32px rgba(0, 85, 212, 0.15);
|
|
.history-header {
|
padding: 18px 20px;
|
border-bottom: 1px solid rgba(0, 85, 212, 0.12);
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
font-weight: 600;
|
font-size: 14px;
|
color: $deep-blue;
|
background: linear-gradient(135deg, rgba(0, 85, 212, 0.08) 0%, rgba(0, 136, 232, 0.05) 100%);
|
position: relative;
|
|
&::after {
|
content: '';
|
position: absolute;
|
bottom: 0;
|
left: 0;
|
right: 0;
|
height: 1px;
|
background: linear-gradient(90deg, transparent, rgba(0, 85, 212, 0.2), transparent);
|
}
|
}
|
|
.session-list {
|
flex: 1;
|
overflow-y: auto;
|
padding: 12px 16px;
|
|
&::-webkit-scrollbar {
|
width: 8px;
|
}
|
&::-webkit-scrollbar-thumb {
|
background: linear-gradient(180deg, $secondary-blue, $primary-blue);
|
border-radius: 4px;
|
box-shadow: 0 0 6px rgba(0, 85, 212, 0.25);
|
}
|
|
.session-item {
|
display: flex;
|
align-items: center;
|
padding: 14px 16px;
|
margin-bottom: 6px;
|
border-radius: 12px;
|
cursor: pointer;
|
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
gap: 12px;
|
position: relative;
|
border: 1px solid transparent;
|
background: #fff;
|
animation: sessionSlideIn 0.35s ease;
|
|
@keyframes sessionSlideIn {
|
from {
|
opacity: 0;
|
transform: translateX(-15px);
|
}
|
to {
|
opacity: 1;
|
transform: translateX(0);
|
}
|
}
|
|
&:hover {
|
background: linear-gradient(135deg, rgba(0, 85, 212, 0.06) 0%, rgba(0, 136, 232, 0.08) 100%);
|
border-color: rgba(0, 85, 212, 0.12);
|
box-shadow: 0 4px 16px rgba(0, 85, 212, 0.1);
|
transform: translateX(4px);
|
|
.delete-btn {
|
opacity: 1;
|
transform: scale(1);
|
}
|
}
|
|
&.active {
|
background: linear-gradient(135deg, rgba(0, 85, 212, 0.12) 0%, rgba(0, 136, 232, 0.15) 100%);
|
border-color: rgba(0, 85, 212, 0.25);
|
color: $primary-blue;
|
box-shadow: 0 4px 16px rgba(0, 85, 212, 0.15);
|
|
.el-icon {
|
color: $primary-blue;
|
}
|
}
|
|
.el-icon {
|
font-size: 18px;
|
flex-shrink: 0;
|
color: $secondary-blue;
|
transition: color 0.2s;
|
}
|
|
.session-name {
|
flex: 1;
|
font-size: 13px;
|
white-space: nowrap;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
color: #1a1a2e;
|
font-weight: 500;
|
}
|
|
.delete-btn {
|
opacity: 0;
|
transform: scale(0.8);
|
transition: all 0.25s ease;
|
padding: 6px;
|
border-radius: 6px;
|
color: #c0c4cc;
|
|
&:hover {
|
color: #fff;
|
background: rgba(245, 108, 108, 0.85);
|
transform: scale(1.1) rotate(8deg);
|
}
|
}
|
}
|
}
|
}
|
|
.chat-main {
|
display: flex;
|
flex-direction: column;
|
height: 100%;
|
flex: 1;
|
overflow: hidden;
|
}
|
|
.message-list {
|
flex: 1;
|
overflow-y: auto;
|
padding: 24px 20px;
|
display: flex;
|
flex-direction: column;
|
gap: 20px;
|
background: linear-gradient(180deg, transparent 0%, rgba(0, 85, 212, 0.02) 100%);
|
|
&::-webkit-scrollbar {
|
width: 8px;
|
}
|
&::-webkit-scrollbar-thumb {
|
background: linear-gradient(180deg, $secondary-blue, $primary-blue);
|
border-radius: 4px;
|
box-shadow: 0 0 8px rgba(0, 85, 212, 0.3);
|
}
|
}
|
|
.message-item {
|
display: flex;
|
gap: 14px;
|
width: 100%;
|
animation: messageSlideIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
@keyframes messageSlideIn {
|
from {
|
opacity: 0;
|
transform: translateY(20px) scale(0.95);
|
}
|
to {
|
opacity: 1;
|
transform: translateY(0) scale(1);
|
}
|
}
|
|
.avatar {
|
width: 42px;
|
height: 42px;
|
border-radius: 14px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
flex-shrink: 0;
|
font-size: 24px;
|
position: relative;
|
overflow: hidden;
|
|
&::before {
|
content: '';
|
position: absolute;
|
inset: 0;
|
background: inherit;
|
filter: blur(10px);
|
opacity: 0.5;
|
z-index: -1;
|
}
|
|
&::after {
|
content: '';
|
position: absolute;
|
top: -50%;
|
left: -50%;
|
width: 200%;
|
height: 200%;
|
background: linear-gradient(45deg, transparent 40%, rgba(255, 255, 255, 0.2) 50%, transparent 60%);
|
animation: shimmer 3s infinite;
|
}
|
}
|
|
.message-content {
|
flex: 1;
|
overflow-x: hidden;
|
display: flex;
|
flex-direction: column;
|
max-width: calc(100% - 56px);
|
|
.text-box {
|
padding: 14px 20px;
|
border-radius: 18px;
|
font-size: 14px;
|
line-height: 1.7;
|
word-break: break-word;
|
max-width: 100%;
|
width: fit-content;
|
overflow-x: auto;
|
transition: all 0.3s ease;
|
position: relative;
|
|
&::-webkit-scrollbar {
|
height: 4px;
|
}
|
&::-webkit-scrollbar-thumb {
|
background: rgba(0, 85, 212, 0.25);
|
border-radius: 2px;
|
}
|
}
|
}
|
|
&.bot-message {
|
.message-content {
|
align-items: flex-start;
|
}
|
.avatar {
|
background: $gradient-dark;
|
color: #fff;
|
box-shadow: 0 6px 20px rgba(0, 85, 212, 0.35);
|
}
|
.text-box {
|
background: #fff;
|
color: #1a1a2e;
|
box-shadow: $shadow-card;
|
border: 1px solid rgba(0, 85, 212, 0.08);
|
border-top-left-radius: 6px;
|
|
&::before {
|
content: '';
|
position: absolute;
|
top: 0;
|
left: 0;
|
right: 0;
|
height: 1px;
|
background: linear-gradient(90deg, rgba(0, 85, 212, 0.15), transparent);
|
}
|
}
|
}
|
|
&.user-message {
|
flex-direction: row-reverse;
|
.message-content {
|
align-items: flex-end;
|
}
|
.avatar {
|
background: linear-gradient(145deg, #5a9fe0, #3d8bd4);
|
color: #fff;
|
box-shadow: 0 6px 20px rgba(0, 85, 212, 0.4);
|
}
|
.text-box {
|
background: $gradient-dark;
|
color: #fff;
|
border-top-right-radius: 6px;
|
box-shadow: 0 6px 24px rgba(0, 85, 212, 0.3);
|
|
&::before {
|
content: '';
|
position: absolute;
|
top: 0;
|
left: 0;
|
right: 0;
|
height: 1px;
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3));
|
}
|
}
|
}
|
}
|
|
@keyframes shimmer {
|
0% {
|
transform: translateX(-100%) rotate(45deg);
|
}
|
100% {
|
transform: translateX(100%) rotate(45deg);
|
}
|
}
|
|
.charts-wrapper {
|
margin-top: 12px;
|
display: flex;
|
flex-direction: column;
|
gap: 12px;
|
overflow-x: auto;
|
width: 100%;
|
padding-bottom: 8px;
|
|
&::-webkit-scrollbar {
|
height: 6px;
|
}
|
&::-webkit-scrollbar-thumb {
|
background: linear-gradient(90deg, $light-blue, $secondary-blue);
|
border-radius: 3px;
|
}
|
}
|
|
.chart-item {
|
width: 100%;
|
min-width: 300px;
|
height: 300px;
|
border-radius: 12px;
|
padding: 12px;
|
margin-bottom: 12px;
|
}
|
|
.table-wrapper {
|
margin-top: 12px;
|
background: #fff;
|
border-radius: 12px;
|
overflow: hidden;
|
overflow-x: auto;
|
width: 100%;
|
box-shadow: $shadow-card;
|
border: 1px solid rgba(0, 122, 255, 0.06);
|
|
&::-webkit-scrollbar {
|
height: 6px;
|
}
|
&::-webkit-scrollbar-thumb {
|
background: linear-gradient(90deg, $light-blue, $secondary-blue);
|
border-radius: 3px;
|
}
|
|
.el-table {
|
min-width: 300px;
|
--el-table-border-color: rgba(0, 122, 255, 0.08);
|
--el-table-header-bg-color: $ice-white;
|
}
|
}
|
|
.input-area {
|
padding: 18px 20px;
|
background: linear-gradient(180deg, rgba(232, 242, 255, 0.95) 0%, #fff 100%);
|
border-top: 1px solid rgba(0, 85, 212, 0.1);
|
position: relative;
|
|
&::before {
|
content: '';
|
position: absolute;
|
top: 0;
|
left: 20px;
|
right: 20px;
|
height: 1px;
|
background: linear-gradient(90deg, transparent, rgba(0, 85, 212, 0.15), transparent);
|
}
|
|
.input-actions {
|
display: flex;
|
gap: 14px;
|
margin-bottom: 12px;
|
align-items: center;
|
|
.file-upload-trigger {
|
display: inline-flex;
|
align-items: center;
|
}
|
|
:deep(.utility-action-btn) {
|
position: relative;
|
height: 34px;
|
padding: 0 14px;
|
border-radius: 999px;
|
border: 1px solid rgba(92, 119, 255, 0.18);
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(236, 243, 255, 0.98));
|
color: $primary-blue;
|
font-weight: 600;
|
box-shadow: 0 10px 20px rgba(0, 85, 212, 0.08);
|
transition: all 0.25s ease;
|
|
.el-icon {
|
margin-right: 5px;
|
}
|
|
&:hover:not(.is-disabled) {
|
color: #fff;
|
border-color: transparent;
|
background: linear-gradient(135deg, #1f6dff 0%, #6b38ef 100%);
|
box-shadow: 0 14px 24px rgba(64, 90, 255, 0.2);
|
transform: translateY(-1px);
|
}
|
}
|
|
:deep(.stop-action-btn) {
|
border-color: rgba(255, 99, 123, 0.18);
|
color: #d33e5e;
|
|
&:hover:not(.is-disabled) {
|
background: linear-gradient(135deg, #f5536e 0%, #a33cff 100%);
|
}
|
}
|
}
|
|
.input-box {
|
padding: 16px;
|
position: relative;
|
background: #fff;
|
border: 2px solid rgba(0, 85, 212, 0.12);
|
border-radius: 16px;
|
margin: 0 4px;
|
transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
&:focus-within {
|
border-color: $primary-blue;
|
box-shadow: 0 0 0 4px rgba(0, 85, 212, 0.12), $shadow-deep;
|
transform: translateY(-2px);
|
background: #fff;
|
}
|
|
.selected-file-tag {
|
display: flex;
|
align-items: center;
|
background: linear-gradient(135deg, rgba(0, 85, 212, 0.1) 0%, rgba(0, 136, 232, 0.15) 100%);
|
border: 1px solid rgba(0, 85, 212, 0.2);
|
border-radius: 10px;
|
padding: 8px 12px;
|
margin-bottom: 12px;
|
gap: 10px;
|
width: fit-content;
|
max-width: 100%;
|
animation: tagSlideIn 0.3s ease;
|
|
@keyframes tagSlideIn {
|
from {
|
opacity: 0;
|
transform: translateX(-10px);
|
}
|
to {
|
opacity: 1;
|
transform: translateX(0);
|
}
|
}
|
|
.el-icon {
|
color: $primary-blue;
|
font-size: 18px;
|
}
|
|
.file-name {
|
font-size: 13px;
|
color: $deep-blue;
|
white-space: nowrap;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
font-weight: 600;
|
}
|
|
.remove-file {
|
cursor: pointer;
|
color: $secondary-blue;
|
transition: all 0.2s;
|
padding: 4px;
|
border-radius: 50%;
|
|
&:hover {
|
color: #fff;
|
background: rgba(245, 108, 108, 0.8);
|
transform: scale(1.1) rotate(90deg);
|
}
|
}
|
}
|
|
:deep(.el-textarea__inner) {
|
padding: 0;
|
padding-bottom: 35px;
|
border: none;
|
box-shadow: none;
|
background: transparent;
|
font-family: inherit;
|
font-size: 14px;
|
line-height: 1.6;
|
color: #1a1a2e;
|
|
&::placeholder {
|
color: #7ab8ff;
|
}
|
|
&:focus {
|
box-shadow: none;
|
}
|
}
|
|
.send-btn {
|
position: absolute;
|
right: 16px;
|
bottom: 16px;
|
padding: 10px 22px;
|
background: $gradient-dark;
|
border: none;
|
border-radius: 10px;
|
font-weight: 600;
|
font-size: 14px;
|
color: #fff;
|
box-shadow: 0 6px 20px rgba(0, 85, 212, 0.4);
|
transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
overflow: hidden;
|
display: inline-flex;
|
align-items: center;
|
gap: 6px;
|
letter-spacing: 0.3px;
|
|
&::before {
|
content: '';
|
position: absolute;
|
top: 0;
|
left: -100%;
|
width: 100%;
|
height: 100%;
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
transition: left 0.5s;
|
}
|
|
&:hover:not(:disabled) {
|
transform: translateY(-3px) scale(1.02);
|
box-shadow: 0 10px 30px rgba(0, 85, 212, 0.5);
|
|
&::before {
|
left: 100%;
|
}
|
}
|
|
&:active:not(:disabled) {
|
transform: translateY(-1px) scale(0.98);
|
}
|
|
&:disabled {
|
background: linear-gradient(145deg, #b0b0b0, #c5c5c5);
|
box-shadow: none;
|
cursor: not-allowed;
|
}
|
|
.el-icon {
|
font-size: 15px;
|
transform: translateY(-1px);
|
}
|
}
|
}
|
}
|
|
.typing-indicator {
|
display: flex;
|
gap: 5px;
|
padding: 10px 14px;
|
background: #fff;
|
border-radius: 14px;
|
width: fit-content;
|
box-shadow: $shadow-card;
|
margin-top: 6px;
|
border: 1px solid rgba(0, 122, 255, 0.06);
|
border-top-left-radius: 4px;
|
|
.dot {
|
width: 7px;
|
height: 7px;
|
background: $secondary-blue;
|
border-radius: 50%;
|
animation: typing 1.4s infinite ease-in-out;
|
|
&:nth-child(2) {
|
animation-delay: 0.2s;
|
background: $primary-blue;
|
}
|
&:nth-child(3) {
|
animation-delay: 0.4s;
|
background: $deep-blue;
|
}
|
}
|
}
|
|
@keyframes typing {
|
0%, 80%, 100% {
|
transform: scale(0.6);
|
opacity: 0.4;
|
}
|
40% {
|
transform: scale(1);
|
opacity: 1;
|
}
|
}
|
|
.code-block {
|
background: linear-gradient(145deg, #1a1a2e, #16213e);
|
color: #a8d8ff;
|
padding: 14px;
|
border-radius: 10px;
|
font-family: 'Fira Code', 'Consolas', monospace;
|
margin: 10px 0;
|
overflow-x: auto;
|
border: 1px solid rgba(90, 200, 250, 0.15);
|
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
&.js-code {
|
color: #5ac8fa;
|
}
|
}
|
|
.chat-main {
|
background:
|
radial-gradient(circle at top left, rgba(46, 140, 224, 0.12) 0%, transparent 34%),
|
linear-gradient(180deg, #fff 0%, #f7fbff 46%, #fff 100%);
|
}
|
|
.chat-hero {
|
display: grid;
|
grid-template-columns: 164px minmax(0, 1fr);
|
gap: 18px;
|
align-items: start;
|
padding: 14px 18px 6px;
|
|
&.compact {
|
grid-template-columns: 122px minmax(0, 1fr);
|
gap: 12px;
|
padding: 8px 18px 2px;
|
}
|
}
|
|
.assistant-stand {
|
position: relative;
|
min-height: 252px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
flex-direction: column;
|
padding-top: 18px;
|
overflow: hidden;
|
|
&.compact {
|
min-height: 176px;
|
padding-top: 8px;
|
}
|
|
&.thinking {
|
.assistant-halo {
|
opacity: 1;
|
transform: scale(1.08);
|
filter: blur(8px);
|
}
|
|
.assistant-scan-ring {
|
opacity: 1;
|
animation-duration: 1.6s;
|
}
|
|
.assistant-orbit {
|
opacity: 1;
|
}
|
|
.assistant-bot {
|
transform: translateY(-4px) scale(1.02);
|
}
|
|
.assistant-bot-head {
|
box-shadow: 0 0 30px rgba(80, 157, 255, 0.36);
|
}
|
|
.assistant-bot-eye {
|
animation: robotBlinkFast 1.1s infinite;
|
box-shadow: 0 0 16px rgba(72, 186, 255, 0.95);
|
}
|
|
.assistant-bot-mouth {
|
width: 28px;
|
opacity: 1;
|
animation: robotTalk 1.2s ease-in-out infinite;
|
}
|
|
.assistant-bot-core {
|
animation: corePulse 1.4s ease-in-out infinite;
|
box-shadow: 0 0 24px rgba(78, 120, 255, 0.26);
|
}
|
|
.assistant-bot-core-ring {
|
animation: coreRotate 3s linear infinite;
|
}
|
|
.assistant-status {
|
color: #6a3bee;
|
box-shadow: 0 10px 22px rgba(106, 59, 238, 0.14);
|
}
|
|
.assistant-status-dot {
|
background: #6a3bee;
|
box-shadow: 0 0 12px rgba(106, 59, 238, 0.9);
|
animation: thinkingDot 1s ease-in-out infinite;
|
}
|
|
.assistant-base-sm {
|
box-shadow: 0 0 24px rgba(255, 93, 122, 0.48);
|
}
|
}
|
}
|
|
.assistant-halo {
|
position: absolute;
|
top: 22px;
|
width: 130px;
|
height: 130px;
|
border-radius: 50%;
|
background: radial-gradient(circle, rgba(46, 140, 224, 0.3) 0%, rgba(0, 85, 212, 0.18) 42%, rgba(113, 54, 244, 0.12) 60%, transparent 78%);
|
filter: blur(6px);
|
opacity: 0.82;
|
transition: all 0.35s ease;
|
}
|
|
.assistant-scan-ring {
|
position: absolute;
|
top: 40px;
|
width: 132px;
|
height: 132px;
|
border-radius: 50%;
|
border: 1px solid rgba(90, 159, 224, 0.22);
|
box-shadow: inset 0 0 16px rgba(255, 255, 255, 0.25);
|
opacity: 0.55;
|
animation: scanRing 4s linear infinite;
|
}
|
|
.assistant-orbit {
|
position: absolute;
|
top: 52px;
|
width: 150px;
|
height: 150px;
|
border-radius: 50%;
|
border: 1px dashed rgba(92, 135, 255, 0.22);
|
opacity: 0.45;
|
}
|
|
.assistant-orbit-a {
|
animation: orbitRotate 8s linear infinite;
|
}
|
|
.assistant-orbit-b {
|
width: 118px;
|
height: 118px;
|
top: 68px;
|
border-color: rgba(255, 108, 150, 0.22);
|
animation: orbitRotateReverse 5.6s linear infinite;
|
}
|
|
.assistant-bot {
|
position: relative;
|
z-index: 1;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
justify-content: center;
|
margin-top: 12px;
|
transition: transform 0.35s ease;
|
}
|
|
.assistant-bot-antenna {
|
position: absolute;
|
top: -4px;
|
width: 4px;
|
height: 20px;
|
border-radius: 999px;
|
background: linear-gradient(180deg, #fefefe, #aac9ff);
|
|
&::before {
|
content: '';
|
position: absolute;
|
top: -6px;
|
left: 50%;
|
width: 10px;
|
height: 10px;
|
border-radius: 50%;
|
transform: translateX(-50%);
|
background: linear-gradient(135deg, #54bfff, #7a41ff);
|
box-shadow: 0 0 14px rgba(84, 191, 255, 0.65);
|
}
|
}
|
|
.assistant-bot-antenna-left {
|
left: 36px;
|
transform: rotate(-14deg);
|
}
|
|
.assistant-bot-antenna-right {
|
right: 36px;
|
transform: rotate(14deg);
|
}
|
|
.assistant-bot-head {
|
position: relative;
|
width: 92px;
|
height: 78px;
|
border-radius: 28px;
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, #e8f1ff 100%);
|
border: 1px solid rgba(0, 85, 212, 0.14);
|
box-shadow: 0 16px 32px rgba(0, 85, 212, 0.14);
|
}
|
|
.assistant-bot-head-glow {
|
position: absolute;
|
inset: 10px 16px auto;
|
height: 20px;
|
border-radius: 999px;
|
background: linear-gradient(180deg, rgba(0, 85, 212, 0.16), transparent);
|
}
|
|
.assistant-bot-eye {
|
position: absolute;
|
top: 30px;
|
width: 16px;
|
height: 16px;
|
border-radius: 50%;
|
background: radial-gradient(circle, #8ef0ff 0%, #56c0ff 42%, #2869ff 100%);
|
box-shadow: 0 0 12px rgba(72, 186, 255, 0.72);
|
animation: robotBlink 3.2s infinite;
|
}
|
|
.assistant-bot-eye-left {
|
left: 22px;
|
}
|
|
.assistant-bot-eye-right {
|
right: 22px;
|
}
|
|
.assistant-bot-mouth {
|
position: absolute;
|
left: 50%;
|
bottom: 16px;
|
width: 22px;
|
height: 4px;
|
transform: translateX(-50%);
|
border-radius: 999px;
|
background: linear-gradient(90deg, rgba(72, 186, 255, 0.2), rgba(72, 186, 255, 0.9), rgba(72, 186, 255, 0.2));
|
}
|
|
.assistant-bot-neck {
|
width: 16px;
|
height: 10px;
|
border-radius: 0 0 10px 10px;
|
background: linear-gradient(180deg, #dceaff, #bdd5ff);
|
margin-top: -2px;
|
}
|
|
.assistant-bot-body {
|
position: relative;
|
width: 104px;
|
height: 92px;
|
margin-top: 2px;
|
border-radius: 28px 28px 34px 34px;
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, #e3eeff 100%);
|
border: 1px solid rgba(0, 85, 212, 0.14);
|
box-shadow: 0 18px 36px rgba(0, 85, 212, 0.16);
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.assistant-bot-arm {
|
position: absolute;
|
top: 18px;
|
width: 16px;
|
height: 44px;
|
border-radius: 999px;
|
background: linear-gradient(180deg, #eff5ff, #c7dbff);
|
border: 1px solid rgba(0, 85, 212, 0.12);
|
}
|
|
.assistant-bot-arm-left {
|
left: -10px;
|
transform: rotate(16deg);
|
}
|
|
.assistant-bot-arm-right {
|
right: -10px;
|
transform: rotate(-16deg);
|
}
|
|
.assistant-bot-core {
|
position: relative;
|
width: 46px;
|
height: 46px;
|
border-radius: 50%;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
color: $primary-blue;
|
background: radial-gradient(circle, rgba(255, 255, 255, 1) 0%, #dae8ff 55%, #adc7ff 100%);
|
}
|
|
.assistant-bot-core-ring {
|
position: absolute;
|
inset: -6px;
|
border-radius: 50%;
|
border: 1px solid rgba(88, 135, 255, 0.3);
|
border-top-color: rgba(255, 96, 139, 0.85);
|
border-right-color: rgba(79, 145, 255, 0.9);
|
}
|
|
.assistant-status {
|
position: relative;
|
z-index: 1;
|
margin-top: 14px;
|
padding: 6px 12px;
|
border-radius: 999px;
|
font-size: 12px;
|
font-weight: 600;
|
color: $deep-blue;
|
background: rgba(255, 255, 255, 0.95);
|
border: 1px solid rgba(0, 85, 212, 0.12);
|
box-shadow: 0 8px 20px rgba(0, 85, 212, 0.08);
|
display: inline-flex;
|
align-items: center;
|
gap: 6px;
|
}
|
|
.assistant-status-dot {
|
width: 8px;
|
height: 8px;
|
border-radius: 50%;
|
background: #2e8ce0;
|
box-shadow: 0 0 10px rgba(46, 140, 224, 0.72);
|
}
|
|
.assistant-base {
|
position: absolute;
|
bottom: 0;
|
left: 50%;
|
transform: translateX(-50%);
|
border-radius: 50%;
|
border: 2px solid rgba(255, 93, 122, 0.22);
|
background: radial-gradient(circle, rgba(255, 255, 255, 0.9) 0%, rgba(255, 111, 145, 0.1) 70%, transparent 100%);
|
}
|
|
.assistant-base-lg {
|
width: 118px;
|
height: 30px;
|
}
|
|
.assistant-base-md {
|
bottom: 6px;
|
width: 88px;
|
height: 20px;
|
border-color: rgba(255, 93, 122, 0.34);
|
}
|
|
.assistant-base-sm {
|
bottom: 11px;
|
width: 54px;
|
height: 10px;
|
background: linear-gradient(90deg, rgba(255, 93, 122, 0.95), rgba(255, 173, 188, 0.9));
|
border: none;
|
box-shadow: 0 0 18px rgba(255, 93, 122, 0.38);
|
}
|
|
@keyframes robotBlink {
|
0%, 44%, 48%, 100% {
|
transform: scaleY(1);
|
}
|
46% {
|
transform: scaleY(0.14);
|
}
|
}
|
|
@keyframes robotBlinkFast {
|
0%, 100% {
|
transform: scaleY(1);
|
}
|
50% {
|
transform: scaleY(0.3);
|
}
|
}
|
|
@keyframes robotTalk {
|
0%, 100% {
|
transform: translateX(-50%) scaleX(1);
|
}
|
50% {
|
transform: translateX(-50%) scaleX(1.35);
|
}
|
}
|
|
@keyframes corePulse {
|
0%, 100% {
|
transform: scale(1);
|
filter: brightness(1);
|
}
|
50% {
|
transform: scale(1.08);
|
filter: brightness(1.08);
|
}
|
}
|
|
@keyframes coreRotate {
|
from {
|
transform: rotate(0deg);
|
}
|
to {
|
transform: rotate(360deg);
|
}
|
}
|
|
@keyframes orbitRotate {
|
from {
|
transform: rotate(0deg);
|
}
|
to {
|
transform: rotate(360deg);
|
}
|
}
|
|
@keyframes orbitRotateReverse {
|
from {
|
transform: rotate(360deg);
|
}
|
to {
|
transform: rotate(0deg);
|
}
|
}
|
|
@keyframes scanRing {
|
0%, 100% {
|
transform: scale(0.96);
|
opacity: 0.42;
|
}
|
50% {
|
transform: scale(1.04);
|
opacity: 0.86;
|
}
|
}
|
|
@keyframes thinkingDot {
|
0%, 100% {
|
transform: scale(1);
|
}
|
50% {
|
transform: scale(1.35);
|
}
|
}
|
|
.welcome-card {
|
position: relative;
|
padding: 14px 14px 12px;
|
border-radius: 16px;
|
background:
|
linear-gradient(#fff, #fff) padding-box,
|
linear-gradient(135deg, rgba(255, 64, 96, 0.85), rgba(117, 65, 255, 0.9)) border-box;
|
border: 1px solid transparent;
|
box-shadow: 0 16px 36px rgba(0, 85, 212, 0.12);
|
|
&.compact {
|
padding: 10px 12px;
|
border-radius: 12px;
|
box-shadow: 0 8px 16px rgba(0, 85, 212, 0.07);
|
|
.welcome-eyebrow {
|
margin-bottom: 4px;
|
}
|
|
.welcome-title {
|
font-size: 17px;
|
line-height: 1.3;
|
|
br {
|
display: none;
|
}
|
}
|
|
.welcome-desc {
|
margin-top: 6px;
|
font-size: 12px;
|
line-height: 1.55;
|
}
|
|
.quick-prompt-list {
|
margin-top: 10px;
|
gap: 6px;
|
}
|
|
.quick-prompt-btn {
|
padding: 8px 10px;
|
font-size: 12px;
|
border-radius: 7px;
|
}
|
|
.more-prompts-btn {
|
margin-top: 8px;
|
font-size: 12px;
|
}
|
}
|
}
|
|
.welcome-eyebrow {
|
font-size: 11px;
|
font-weight: 700;
|
letter-spacing: 2px;
|
color: rgba(0, 85, 212, 0.58);
|
margin-bottom: 8px;
|
}
|
|
.welcome-title {
|
margin: 0;
|
font-size: 26px;
|
line-height: 1.2;
|
font-weight: 800;
|
color: #172033;
|
}
|
|
.welcome-desc {
|
margin: 10px 0 0;
|
font-size: 13px;
|
line-height: 1.7;
|
color: #5f6980;
|
}
|
|
.quick-prompt-list {
|
display: grid;
|
gap: 8px;
|
margin-top: 14px;
|
}
|
|
.quick-prompt-btn {
|
width: 100%;
|
border: none;
|
border-radius: 10px;
|
padding: 11px 14px;
|
text-align: left;
|
font-size: 13px;
|
font-weight: 600;
|
color: #fff;
|
cursor: pointer;
|
background: linear-gradient(90deg, #ff4c55 0%, #7c38ef 100%);
|
box-shadow: 0 12px 22px rgba(124, 56, 239, 0.18);
|
transition: transform 0.25s ease, box-shadow 0.25s ease, opacity 0.2s ease;
|
position: relative;
|
overflow: hidden;
|
|
&::before {
|
content: '';
|
position: absolute;
|
inset: 0;
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.22), transparent 56%);
|
pointer-events: none;
|
}
|
|
&::after {
|
content: '';
|
position: absolute;
|
top: -120%;
|
left: -30%;
|
width: 45%;
|
height: 260%;
|
background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
transform: rotate(22deg);
|
opacity: 0;
|
transition: all 0.35s ease;
|
}
|
|
&:hover:not(:disabled) {
|
transform: translateY(-2px) scale(1.01);
|
box-shadow: 0 16px 28px rgba(124, 56, 239, 0.24);
|
|
&::after {
|
left: 100%;
|
opacity: 1;
|
}
|
}
|
|
&:disabled {
|
cursor: not-allowed;
|
opacity: 0.65;
|
}
|
}
|
|
.more-prompts-btn {
|
margin-top: 10px;
|
padding: 0 12px;
|
height: 32px;
|
border: 1px solid rgba(208, 65, 81, 0.12);
|
border-radius: 999px;
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(255, 241, 245, 0.96));
|
color: #d04151;
|
font-size: 13px;
|
font-weight: 600;
|
cursor: pointer;
|
display: inline-flex;
|
align-items: center;
|
gap: 6px;
|
box-shadow: 0 10px 18px rgba(208, 65, 81, 0.08);
|
transition: all 0.25s ease;
|
|
&:hover {
|
transform: translateY(-1px);
|
background: linear-gradient(135deg, #ff5570 0%, #8a3df6 100%);
|
border-color: transparent;
|
color: #fff;
|
box-shadow: 0 14px 24px rgba(138, 61, 246, 0.18);
|
}
|
}
|
|
.hero-dot-grid {
|
display: grid;
|
grid-template-columns: repeat(14, 1fr);
|
gap: 7px;
|
padding: 0 18px 14px;
|
|
span {
|
display: block;
|
width: 100%;
|
aspect-ratio: 1;
|
border-radius: 2px;
|
background: linear-gradient(135deg, rgba(255, 110, 138, 0.95), rgba(255, 190, 201, 0.55));
|
}
|
}
|
|
.message-list {
|
padding: 8px 18px 18px;
|
gap: 16px;
|
background: transparent;
|
}
|
|
.input-area {
|
padding: 12px 18px 16px;
|
background: #fff;
|
border-top: none;
|
|
&::before {
|
display: none;
|
}
|
|
.input-box {
|
padding: 14px 16px 16px;
|
border: 1px solid rgba(123, 56, 239, 0.9);
|
border-radius: 22px;
|
margin: 0;
|
transition: all 0.25s ease;
|
box-shadow: 0 14px 34px rgba(0, 85, 212, 0.08);
|
|
&:focus-within {
|
border-color: #7c38ef;
|
box-shadow: 0 0 0 3px rgba(124, 56, 239, 0.1), 0 18px 40px rgba(0, 85, 212, 0.12);
|
transform: none;
|
}
|
|
:deep(.el-textarea__inner) {
|
padding-right: 58px;
|
padding-bottom: 0;
|
min-height: 104px;
|
|
&::placeholder {
|
color: #a0a9bc;
|
}
|
}
|
|
.send-btn {
|
right: 25px;
|
top: 50%;
|
transform: translateY(-50%);
|
width: 36px;
|
min-width: 36px;
|
height: 36px;
|
padding: 0;
|
background: linear-gradient(135deg, #ff5570 0%, #7a36f2 58%, #2d79ff 100%);
|
border-radius: 50%;
|
box-shadow: 0 12px 24px rgba(109, 50, 236, 0.24);
|
transition: all 0.25s ease;
|
gap: 0;
|
|
&:hover:not(:disabled) {
|
transform: translateY(calc(-50% - 1px)) scale(1.04);
|
box-shadow: 0 16px 28px rgba(109, 50, 236, 0.3);
|
}
|
|
&:active:not(:disabled) {
|
transform: translateY(-50%) scale(0.96);
|
}
|
|
.el-icon {
|
margin: 0;
|
font-size: 16px;
|
transform: translate(0, -1px);
|
}
|
}
|
}
|
}
|
|
@media (max-width: 767px) {
|
.chat-hero {
|
grid-template-columns: 1fr;
|
gap: 10px;
|
padding: 14px 14px 6px;
|
|
&.compact {
|
padding: 8px 14px 4px;
|
}
|
}
|
|
.assistant-stand {
|
min-height: 184px;
|
}
|
|
.welcome-card {
|
padding: 12px 12px 10px;
|
}
|
|
.welcome-title {
|
font-size: 21px;
|
}
|
|
.hero-dot-grid {
|
grid-template-columns: repeat(12, 1fr);
|
gap: 6px;
|
padding: 0 14px 12px;
|
}
|
|
.message-list {
|
padding: 8px 14px 14px;
|
}
|
|
.input-area {
|
padding: 10px 14px 14px;
|
}
|
|
.input-area .input-actions {
|
gap: 10px;
|
flex-wrap: wrap;
|
}
|
}
|
</style>
|