From 04b1a9cfde4049be9a38b9832d5289d4a192c883 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期五, 15 五月 2026 16:29:33 +0800
Subject: [PATCH] 加班申请模块和审批流程公共组件
---
src/components/AIChatSidebar/index.vue | 1402 ++++++++++++++++++++++++++++++++++++++++------------------
1 files changed, 960 insertions(+), 442 deletions(-)
diff --git a/src/components/AIChatSidebar/index.vue b/src/components/AIChatSidebar/index.vue
index 369d75d..a2a365a 100644
--- a/src/components/AIChatSidebar/index.vue
+++ b/src/components/AIChatSidebar/index.vue
@@ -1,7 +1,7 @@
<template>
<div class="ai-chat-sidebar-wrapper">
<!-- 鎮诞鍥炬爣 -->
- <div class="ai-chat-trigger" @click="toggleSidebar" v-show="!visible">
+ <div v-if="!hideTrigger" 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>
@@ -12,14 +12,16 @@
<!-- 渚ц竟鏍忓璇濇 -->
<el-drawer
v-model="visible"
- :size="drawerSize"
- direction="rtl"
+ :size="computedDrawerSize"
+ :direction="drawerDirection"
:with-header="true"
class="ai-chat-drawer"
:modal="false"
modal-class="ai-chat-overlay"
:show-close="false"
:append-to-body="false"
+ :close-on-press-escape="!hideTrigger"
+ :close-on-click-modal="!hideTrigger"
@close="handleClose"
>
<template #header>
@@ -50,8 +52,16 @@
<el-icon :size="18"><Plus /></el-icon>
</el-button>
</el-tooltip>
- <div class="action-divider"></div>
- <el-tooltip content="鍏抽棴" placement="bottom">
+ <el-button
+ v-if="headerExtraActionText"
+ link
+ class="header-action-btn header-action-btn--text"
+ @click="handleHeaderExtraAction"
+ >
+ {{ headerExtraActionText }}
+ </el-button>
+ <div v-if="!hideTrigger" class="action-divider"></div>
+ <el-tooltip v-if="!hideTrigger" content="鍏抽棴" placement="bottom">
<el-button link class="header-action-btn close-btn" @click="handleManualClose">
<el-icon :size="18"><Close /></el-icon>
</el-button>
@@ -105,23 +115,17 @@
<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 class="assistant-model-shell">
+ <div class="assistant-model-cut">
+ <img
+ v-if="currentAssistantAvatar"
+ class="assistant-model-img"
+ :src="currentAssistantAvatar"
+ :alt="currentAssistant.label"
+ />
+ <div v-else class="assistant-model-fallback">
+ <el-icon :size="30"><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">
@@ -169,10 +173,6 @@
</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"
@@ -186,6 +186,37 @@
<div class="message-content">
<!-- 鏂囨湰鍐呭 -->
<div class="text-box" v-html="message.htmlContent"></div>
+
+ <div v-if="message.localUploadFiles?.length" class="message-local-file-list">
+ <div
+ v-for="(file, fileIndex) in message.localUploadFiles"
+ :key="`${file.previewId || file.name}-${fileIndex}`"
+ :class="['message-local-file-item', { clickable: !!file.accessUrl && !file.isImage }]"
+ @click="handleMessageFileClick(file)"
+ >
+ <el-image
+ v-if="file.isImage && file.previewUrl"
+ :src="file.previewUrl"
+ :preview-src-list="getImagePreviewList(message.localUploadFiles)"
+ :initial-index="getImagePreviewInitialIndex(message.localUploadFiles, file.previewUrl)"
+ :z-index="4000"
+ preview-teleported
+ fit="cover"
+ class="message-local-file-thumb"
+ />
+ <el-icon v-else class="message-local-file-icon"><Document /></el-icon>
+ <div class="message-local-file-meta">
+ <span
+ :class="['message-local-file-name', { clickable: !!file.accessUrl }]"
+ :title="file.name"
+ @click.stop="openMessageAttachment(file)"
+ >
+ {{ file.name }}
+ </span>
+ <small v-if="Number(file.size) > 0" class="message-local-file-size">{{ formatFileSize(file.size) }}</small>
+ </div>
+ </div>
+ </div>
<!-- 鍥捐〃鍐呭 -->
<div v-if="message.chartOptions && message.chartRenderReady" class="charts-wrapper">
@@ -443,9 +474,22 @@
</div>
<div class="input-box">
<div v-if="selectedFiles.length" class="selected-file-list">
- <div v-for="(file, fileIndex) in selectedFiles" :key="`${file.name}-${fileIndex}`" class="selected-file-tag">
- <el-icon><Document /></el-icon>
- <span class="file-name">{{ file.name }}</span>
+ <div v-for="(file, fileIndex) in selectedFileSnapshots" :key="`${file.previewId || file.name}-${fileIndex}`" class="selected-file-tag">
+ <el-image
+ v-if="file.isImage && file.previewUrl"
+ :src="file.previewUrl"
+ :preview-src-list="getImagePreviewList(selectedFileSnapshots)"
+ :initial-index="getImagePreviewInitialIndex(selectedFileSnapshots, file.previewUrl)"
+ :z-index="4000"
+ preview-teleported
+ fit="cover"
+ class="selected-file-thumb"
+ />
+ <el-icon v-else><Document /></el-icon>
+ <div class="selected-file-meta">
+ <span class="file-name">{{ file.name }}</span>
+ <small class="file-size">{{ formatFileSize(file.size) }}</small>
+ </div>
<el-icon class="remove-file" @click="removeSelectedFile(fileIndex)"><Close /></el-icon>
</div>
</div>
@@ -478,8 +522,17 @@
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 { Cpu, User, Plus, Timer, Delete, ChatDotSquare, VideoPause, Upload, Document, Close, Promotion, RefreshRight } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
+import { builtInAssistants, generalAssistant } from './assistants'
+import todoAssistantAvatar from '@/assets/AI/寰呭姙鍔╂墜.png'
+import salesAssistantAvatar from '@/assets/AI/閿�鍞姪鎵�.png'
+import purchaseAssistantAvatar from '@/assets/AI/閲囪喘鍔╂墜.png'
+import productionAssistantAvatar from '@/assets/AI/鐢熶骇鍔╂墜.png'
+import financeAssistantAvatar from '@/assets/AI/璐㈠姟鍔╂墜.png'
+import bossAssistantAvatar from '@/assets/AI/寰呭姙鍔╂墜.png'
+
+const emit = defineEmits(['header-extra-action'])
const props = defineProps({
assistants: {
@@ -489,72 +542,51 @@
defaultAssistant: {
type: String,
default: ''
+ },
+ hideTrigger: {
+ type: Boolean,
+ default: false
+ },
+ autoOpen: {
+ type: Boolean,
+ default: false
+ },
+ drawerSize: {
+ type: [String, Number],
+ default: ''
+ },
+ drawerDirection: {
+ type: String,
+ default: 'rtl'
+ },
+ headerExtraActionText: {
+ type: String,
+ default: ''
}
})
-const builtInAssistants = [
- {
- key: 'general',
- label: '寰呭姙鍔╃悊',
- title: '寰呭姙鏅鸿兘鍔╃悊',
- tooltip: '寰呭姙鍔╂墜',
- icon: Cpu,
- apiBase: '/xiaozhi',
- storageKey: 'ai_chat_uuid',
- placeholder: '璇疯緭鍏ユ偍鐨勯棶棰�... (Enter 鍙戦��, Shift+Enter 鎹㈣)',
- welcomeMessage: '浣犲ソ',
- description: '鎴戝彲浠ュ洖绛斾綘鐨勯棶棰橈紝涓轰綘鎻愪緵涓氬姟鏁版嵁瑙h淇℃伅銆佸鐞嗗缓璁拰杈呭姪鍐崇瓥鏀寔銆�',
- 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: true,
- allowMultipleFileUpload: true,
- fileAnalyzeUrl: '/purchase-ai/analyze-files',
- emptySessionText: '鏆傛棤閲囪喘浼氳瘽'
- }
-]
-
+const hideTrigger = computed(() => props.hideTrigger)
+const headerExtraActionText = computed(() => String(props.headerExtraActionText || '').trim())
+const drawerDirection = computed(() => (props.drawerDirection === 'ttb' || props.drawerDirection === 'btt' || props.drawerDirection === 'ltr' || props.drawerDirection === 'rtl')
+ ? props.drawerDirection
+ : 'rtl')
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 assistantAvatarByKey = {
+ general: todoAssistantAvatar,
+ todo: todoAssistantAvatar,
+ purchase: purchaseAssistantAvatar,
+ sales: salesAssistantAvatar,
+ production: productionAssistantAvatar,
+ finance: financeAssistantAvatar,
+ boss: bossAssistantAvatar
}
+const currentAssistantAvatar = computed(() => {
+ const assistant = currentAssistant.value || {}
+ return assistant.avatar || assistantAvatarByKey[assistant.key] || ''
+})
+const showAssistantSwitch = computed(() => assistants.value.length > 1)
const quickPromptLimit = 3
const quickPromptStart = ref(0)
const quickPrompts = computed(() => {
@@ -562,7 +594,7 @@
if (Array.isArray(assistant.quickPrompts) && assistant.quickPrompts.length) {
return assistant.quickPrompts
}
- return assistantQuickPromptMap[assistant.key] || assistantQuickPromptMap.general
+ return generalAssistant.quickPrompts || []
})
const displayedQuickPrompts = computed(() => {
const prompts = quickPrompts.value || []
@@ -578,17 +610,19 @@
const visible = ref(false)
const windowWidth = ref(window.innerWidth)
-const drawerSize = computed(() => {
+const responsiveDrawerSize = computed(() => {
if (windowWidth.value < 768) return '100%'
if (windowWidth.value < 1200) return '50%'
return '50%'
})
+const computedDrawerSize = computed(() => props.drawerSize || responsiveDrawerSize.value)
const messageListRef = ref(null)
const isSending = ref(false)
const currentAbortController = ref(null)
const inputMessage = ref('')
const selectedFiles = ref([])
const uploadFileList = ref([])
+const selectedFileSnapshots = ref([])
const messages = ref([])
const uuid = ref('')
const chartInstances = ref({})
@@ -792,6 +826,142 @@
const sessions = ref([])
const loadingSessions = ref(false)
+const isImageFileType = (fileType = '') => String(fileType || '').toLowerCase().startsWith('image/')
+const imageFilePathPattern = /\.(png|jpe?g|gif|webp|bmp|svg)$/i
+
+const getPathnameFromFilePath = (filePath = '') => {
+ const rawPath = String(filePath || '').trim()
+ if (!rawPath) return ''
+ try {
+ const baseOrigin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
+ return new URL(rawPath, baseOrigin).pathname || ''
+ } catch (err) {
+ return rawPath.split('?')[0]
+ }
+}
+
+const isImageFilePath = (filePath = '') => {
+ const pathname = getPathnameFromFilePath(filePath).toLowerCase()
+ return imageFilePathPattern.test(pathname)
+}
+
+const getHistoryFileName = (filePath = '', index = 0) => {
+ const pathname = getPathnameFromFilePath(filePath)
+ const fileName = pathname.split('/').filter(Boolean).pop()
+ if (!fileName) return `file-${index + 1}`
+ try {
+ return decodeURIComponent(fileName)
+ } catch (err) {
+ return fileName
+ }
+}
+
+const getImagePreviewList = (files = []) => {
+ if (!Array.isArray(files)) return []
+ return files
+ .filter(item => item?.isImage && item?.previewUrl)
+ .map(item => item.previewUrl)
+}
+
+const getImagePreviewInitialIndex = (files = [], previewUrl = '') => {
+ const list = getImagePreviewList(files)
+ const index = list.indexOf(previewUrl)
+ return index >= 0 ? index : 0
+}
+
+const formatFileSize = (size) => {
+ const bytes = Number(size)
+ if (!Number.isFinite(bytes) || bytes <= 0) return '0 B'
+ if (bytes < 1024) return `${bytes} B`
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1).replace(/\.0$/, '')} KB`
+ return `${(bytes / (1024 * 1024)).toFixed(1).replace(/\.0$/, '')} MB`
+}
+
+const createLocalFileSnapshot = (file, index = 0) => {
+ const rawFile = typeof File !== 'undefined' && file instanceof File ? file : null
+ const fileType = rawFile?.type || ''
+ const isImage = isImageFileType(fileType)
+ const canCreateObjectURL = typeof URL !== 'undefined' && typeof URL.createObjectURL === 'function'
+ const previewUrl = isImage && rawFile && canCreateObjectURL ? URL.createObjectURL(rawFile) : ''
+ return {
+ previewId: `${rawFile?.name || 'file'}-${rawFile?.size || 0}-${rawFile?.lastModified || Date.now()}-${index}`,
+ name: rawFile?.name || `file-${index + 1}`,
+ size: rawFile?.size || 0,
+ type: fileType,
+ isImage,
+ previewUrl,
+ accessUrl: '',
+ rawFile,
+ isObjectUrl: !!previewUrl
+ }
+}
+
+const createHistoryFileSnapshot = (filePath, memoryId = '', messageIndex = 0, fileIndex = 0) => {
+ const normalizedPath = String(filePath || '').trim()
+ if (!normalizedPath) return null
+ const isImage = isImageFilePath(normalizedPath)
+ return {
+ previewId: `${memoryId || 'history'}-${messageIndex}-${fileIndex}`,
+ name: getHistoryFileName(normalizedPath, fileIndex),
+ size: 0,
+ type: '',
+ isImage,
+ previewUrl: isImage ? normalizedPath : '',
+ accessUrl: normalizedPath,
+ rawFile: null,
+ isObjectUrl: false
+ }
+}
+
+const revokeLocalFileSnapshots = (snapshots = []) => {
+ const canRevokeObjectURL = typeof URL !== 'undefined' && typeof URL.revokeObjectURL === 'function'
+ if (!canRevokeObjectURL) return
+ if (!Array.isArray(snapshots)) return
+ snapshots.forEach((snapshot) => {
+ if (snapshot?.isObjectUrl && snapshot?.previewUrl) {
+ URL.revokeObjectURL(snapshot.previewUrl)
+ }
+ })
+}
+
+const mapHistoryFilePathsToSnapshots = (filePaths = [], memoryId = '', messageIndex = 0) => {
+ if (!Array.isArray(filePaths)) return []
+ return filePaths
+ .map((filePath, fileIndex) => createHistoryFileSnapshot(filePath, memoryId, messageIndex, fileIndex))
+ .filter(Boolean)
+}
+
+const openMessageAttachment = (file) => {
+ const accessUrl = String(file?.accessUrl || '').trim()
+ if (!accessUrl) return
+ if (typeof window === 'undefined' || typeof window.open !== 'function') return
+ window.open(accessUrl, '_blank', 'noopener,noreferrer')
+}
+
+const handleMessageFileClick = (file) => {
+ if (!file?.accessUrl || file?.isImage) return
+ openMessageAttachment(file)
+}
+
+const revokeMessageLocalFileSnapshots = (messageList = []) => {
+ if (!Array.isArray(messageList)) return
+ messageList.forEach((msg) => {
+ if (Array.isArray(msg?.localUploadFiles)) {
+ revokeLocalFileSnapshots(msg.localUploadFiles)
+ msg.localUploadFiles = []
+ }
+ })
+}
+
+const clearSelectedFiles = ({ releaseSnapshots = true } = {}) => {
+ if (releaseSnapshots) {
+ revokeLocalFileSnapshots(selectedFileSnapshots.value)
+ }
+ selectedFiles.value = []
+ uploadFileList.value = []
+ selectedFileSnapshots.value = []
+}
+
const abortCurrentRequest = () => {
if (!currentAbortController.value) return
@@ -835,6 +1005,7 @@
const selectSession = async (session) => {
showHistory.value = false
+ clearSelectedFiles()
uuid.value = session.memoryId
localStorage.setItem(currentAssistant.value.storageKey, uuid.value)
@@ -842,6 +1013,7 @@
try {
const res = await request.get(`${currentAssistant.value.apiBase}/history/messages/${uuid.value}`)
if (res.code === 200) {
+ revokeMessageLocalFileSnapshots(messages.value)
disposeCharts()
messages.value = []
const historyMsgs = res.data || []
@@ -853,7 +1025,7 @@
const messageObj = {
isUser,
- content: msg.content,
+ content: msg.content || '',
htmlContent: '',
isTyping: false,
chartOptions: null,
@@ -861,7 +1033,8 @@
type: '',
tableData: null,
payloadTreeData: null,
- payloadHiddenData: null
+ payloadHiddenData: null,
+ localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : []
}
messages.value.push(messageObj)
@@ -876,15 +1049,15 @@
}
// 瑙f瀽鍘嗗彶娑堟伅涓殑 JSON
- const extracted = extractEmbeddedSuccessJson(msg.content)
+ const extracted = extractEmbeddedSuccessJson(msg.content || '')
if (extracted) {
applyStructuredMessageData(messageObj, extracted.data, botMsgIndex)
}
- updateOutputState(msg.content, botMsgIndex)
- messageObj.htmlContent = convertStreamOutput(msg.content, botMsgIndex)
+ updateOutputState(msg.content || '', botMsgIndex)
+ messageObj.htmlContent = convertStreamOutput(msg.content || '', botMsgIndex)
} else {
- messageObj.htmlContent = convertTextToHtml(msg.content)
+ messageObj.htmlContent = convertTextToHtml(msg.content || '')
}
})
scrollToBottom()
@@ -919,15 +1092,16 @@
}
onMounted(() => {
- initUUID()
- // 鍒濆娆㈣繋
- if (messages.value.length === 0) {
- hello()
+ if (props.autoOpen) {
+ visible.value = true
}
+ initUUID()
window.addEventListener('resize', handleWindowResize)
})
onUnmounted(() => {
+ revokeMessageLocalFileSnapshots(messages.value)
+ clearSelectedFiles()
disposeCharts()
window.removeEventListener('resize', handleWindowResize)
})
@@ -936,21 +1110,36 @@
if (!prevKey || nextKey === prevKey) return
abortCurrentRequest()
+ revokeMessageLocalFileSnapshots(messages.value)
disposeCharts()
messages.value = []
outputState.value = {}
sessions.value = []
showHistory.value = false
- selectedFiles.value = []
- uploadFileList.value = []
+ clearSelectedFiles()
inputMessage.value = ''
quickPromptStart.value = 0
initUUID()
- hello()
+})
+
+watch(() => props.defaultAssistant, (nextKey) => {
+ if (!nextKey || nextKey === selectedAssistantKey.value) return
+ if (!assistants.value.some(item => item.key === nextKey)) return
+ selectedAssistantKey.value = nextKey
+})
+
+watch(() => props.autoOpen, (nextValue) => {
+ if (nextValue) {
+ visible.value = true
+ }
})
const handleWindowResize = () => {
windowWidth.value = window.innerWidth
+}
+
+const handleHeaderExtraAction = () => {
+ emit('header-extra-action')
}
const toggleSidebar = () => {
@@ -961,10 +1150,12 @@
}
const handleClose = () => {
+ if (hideTrigger.value) return
visible.value = false
}
const handleManualClose = () => {
+ if (hideTrigger.value) return
if (isSending.value) {
abortCurrentRequest()
}
@@ -980,22 +1171,17 @@
uuid.value = storedUUID
}
-const hello = () => {
- sendRequest(currentAssistant.value.welcomeMessage || '浣犲ソ')
-}
-
const newChat = () => {
+ revokeMessageLocalFileSnapshots(messages.value)
disposeCharts()
messages.value = []
outputState.value = {}
sessions.value = []
showHistory.value = false
- selectedFiles.value = []
- uploadFileList.value = []
+ clearSelectedFiles()
quickPromptStart.value = 0
localStorage.removeItem(currentAssistant.value.storageKey)
initUUID()
- hello()
}
const handleNewChat = () => {
@@ -1343,12 +1529,7 @@
if (visibleValue === undefined || visibleValue === null) return clonePurchasePayloadValue(hiddenValue)
if (Array.isArray(visibleValue) && Array.isArray(hiddenValue)) {
- const maxLength = Math.max(visibleValue.length, hiddenValue.length)
- const merged = []
- for (let i = 0; i < maxLength; i++) {
- merged[i] = mergePurchasePayloadWithHidden(visibleValue[i], hiddenValue[i])
- }
- return merged
+ return visibleValue.map((item, index) => mergePurchasePayloadWithHidden(item, hiddenValue[index]))
}
if (
@@ -1667,6 +1848,76 @@
'瀹℃壒鐢ㄦ埛ID鍒楄〃'
])
+const purchaseIntegerFieldKeys = new Set([
+ 'id',
+ 'supplierId',
+ 'recorderId',
+ 'salesContractNoId',
+ 'salesLedgerId',
+ 'Type',
+ 'businessPersonId',
+ 'productId',
+ 'productModelId',
+ 'ticketRegistrationId',
+ 'type',
+ 'approvalStatus',
+ 'inventoryWarningQuantity'
+])
+
+const purchaseDecimalFieldKeys = new Set([
+ 'invoiceAmount',
+ 'contractAmount',
+ 'receiptPaymentAmount',
+ 'unReceiptPaymentAmount',
+ 'quantity',
+ 'taxRate',
+ 'taxInclusiveUnitPrice',
+ 'taxInclusiveTotalPrice',
+ 'taxExclusiveTotalPrice',
+ 'priceWithTax',
+ 'totalPriceWithTax'
+])
+
+const purchaseBooleanFieldKeys = new Set([
+ 'hasChildren',
+ 'isWhite',
+ 'isInspected',
+ 'isChecked'
+])
+
+const purchaseStringFieldKeys = new Set([
+ 'entryDateStart',
+ 'entryDateEnd',
+ 'purchaseContractNumber',
+ 'supplierName',
+ 'recorderName',
+ 'salesContractNo',
+ 'projectName',
+ 'entryDate',
+ 'executionDate',
+ 'remarks',
+ 'attachmentMaterials',
+ 'createdAt',
+ 'updatedAt',
+ 'phoneNumber',
+ 'invoiceNumber',
+ 'paymentMethod',
+ 'templateName',
+ 'productCategory',
+ 'specificationModel',
+ 'unit',
+ 'invoiceType'
+])
+
+const purchaseGenericArrayFieldKeys = new Set([
+ 'purchaseLedgers',
+ 'productData'
+])
+
+const purchaseStringArrayFieldKeys = new Set(['tempFileIds'])
+
+const purchaseObjectArrayFieldKeys = new Set(['SalesLedgerFiles'])
+
const normalizePurchaseProductRecord = (record) => {
if (!record || typeof record !== 'object' || Array.isArray(record)) return record
return mapPayloadKeys(record, purchasePayloadFieldKeyMap)
@@ -1681,6 +1932,23 @@
''
}
+const prunePurchaseProductRecord = (record) => {
+ if (!record || typeof record !== 'object' || Array.isArray(record)) return null
+ const normalizedRecord = normalizePurchaseProductRecord(record)
+ const hasVisibleFieldValue = Object.entries(normalizedRecord).some(([key, value]) => {
+ if (shouldHidePurchaseField(key)) return false
+ return hasMeaningfulPayloadValue(value)
+ })
+ return hasVisibleFieldValue ? normalizedRecord : null
+}
+
+const normalizeAndFilterPurchaseProductData = (value) => {
+ if (!Array.isArray(value)) return value
+ return value
+ .map(item => prunePurchaseProductRecord(item))
+ .filter(Boolean)
+}
+
const mergeLegacyProductDataIntoLedgers = (payload) => {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return payload
if (!Array.isArray(payload.purchaseLedgers) || !Array.isArray(payload.productData) || !payload.productData.length) {
@@ -1689,13 +1957,11 @@
const ledgers = payload.purchaseLedgers.map(ledger => ({
...ledger,
- productData: Array.isArray(ledger.productData)
- ? ledger.productData.map(normalizePurchaseProductRecord)
- : []
+ productData: normalizeAndFilterPurchaseProductData(ledger.productData) || []
}))
const unmatchedProducts = []
- payload.productData.map(normalizePurchaseProductRecord).forEach(product => {
+ normalizeAndFilterPurchaseProductData(payload.productData).forEach(product => {
const productMatchKey = getPurchaseProductMatchKey(product)
const matchedLedger = ledgers.find(ledger => {
const ledgerKeys = [
@@ -1734,9 +2000,7 @@
if (!record || typeof record !== 'object' || Array.isArray(record)) return record
const normalizedRecord = {
...record,
- productData: Array.isArray(record.productData)
- ? record.productData.map(normalizePurchaseProductRecord)
- : record.productData
+ productData: normalizeAndFilterPurchaseProductData(record.productData)
}
return Object.entries(normalizedRecord).reduce((result, [key, value]) => {
if (purchaseLedgerAllowedFieldKeys.has(key)) {
@@ -1746,12 +2010,199 @@
}, {})
}
+const normalizeAttachmentMaterialsValue = (value) => {
+ if (value === null || value === undefined) return value
+ if (typeof value === 'string') return value
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value)
+ try {
+ return JSON.stringify(value)
+ } catch (err) {
+ return String(value)
+ }
+}
+
+const normalizePurchaseAttachmentMaterialsField = (value) => {
+ if (Array.isArray(value)) {
+ return value.map(item => normalizePurchaseAttachmentMaterialsField(item))
+ }
+ if (value && typeof value === 'object') {
+ return Object.entries(value).reduce((result, [key, item]) => {
+ if (key === 'attachmentMaterials') {
+ result[key] = normalizeAttachmentMaterialsValue(item)
+ } else {
+ result[key] = normalizePurchaseAttachmentMaterialsField(item)
+ }
+ return result
+ }, {})
+ }
+ return value
+}
+
+const normalizePurchaseNumericText = (text = '') => {
+ return String(text)
+ .replace(/[,\s锛宂/g, '')
+ .replace(/[楼锟ュ厓%]/g, '')
+}
+
+const parsePurchaseNumberValue = (value) => {
+ if (typeof value === 'number') {
+ return Number.isFinite(value) ? value : null
+ }
+ if (typeof value === 'boolean') {
+ return value ? 1 : 0
+ }
+ if (typeof value !== 'string') {
+ return null
+ }
+ const text = normalizePurchaseNumericText(value.trim())
+ if (!text) return null
+ if (!/^[-+]?\d+(\.\d+)?$/.test(text)) return null
+ const numberValue = Number(text)
+ return Number.isFinite(numberValue) ? numberValue : null
+}
+
+const normalizePurchaseIntegerFieldValue = (value) => {
+ if (value === null || value === undefined) return value
+ if (value === '') return null
+ const numberValue = parsePurchaseNumberValue(value)
+ if (numberValue === null) return null
+ return Math.trunc(numberValue)
+}
+
+const normalizePurchaseDecimalFieldValue = (value) => {
+ if (value === null || value === undefined) return value
+ if (value === '') return null
+ const numberValue = parsePurchaseNumberValue(value)
+ return numberValue === null ? null : numberValue
+}
+
+const normalizePurchaseBooleanFieldValue = (value) => {
+ if (value === null || value === undefined) return value
+ if (value === '') return null
+ if (typeof value === 'boolean') return value
+ if (typeof value === 'number') return value !== 0
+ if (typeof value !== 'string') return null
+
+ const text = value.trim().toLowerCase()
+ if (!text) return null
+ if (['true', '1', 'yes', 'y', '鏄�', '宸�', 'checked'].includes(text)) return true
+ if (['false', '0', 'no', 'n', '鍚�', '鏈�', 'unchecked'].includes(text)) return false
+ return null
+}
+
+const normalizePurchaseStringFieldValue = (value) => {
+ if (value === null || value === undefined) return value
+ if (typeof value === 'string') return value
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value)
+ try {
+ return JSON.stringify(value)
+ } catch (err) {
+ return String(value)
+ }
+}
+
+const normalizePurchaseStringArrayFieldValue = (value) => {
+ if (Array.isArray(value)) {
+ return value
+ .map(item => normalizePurchaseStringFieldValue(item))
+ .filter(item => item !== null && item !== undefined && item !== '')
+ }
+ if (value === null || value === undefined || value === '') return []
+ if (typeof value === 'string') {
+ const text = value.trim()
+ if (!text) return []
+ if (/^\[.*\]$/.test(text)) {
+ try {
+ const parsedValue = JSON.parse(text)
+ if (Array.isArray(parsedValue)) {
+ return parsedValue
+ .map(item => normalizePurchaseStringFieldValue(item))
+ .filter(item => item !== null && item !== undefined && item !== '')
+ }
+ } catch (err) {
+ // Keep as plain text when not valid JSON array.
+ }
+ }
+ const splitValues = text
+ .split(/[,\n锛宂/)
+ .map(item => item.trim())
+ .filter(Boolean)
+ return splitValues.length > 1 ? splitValues : [text]
+ }
+ const normalizedValue = normalizePurchaseStringFieldValue(value)
+ return normalizedValue === null || normalizedValue === undefined || normalizedValue === ''
+ ? []
+ : [normalizedValue]
+}
+
+const normalizePurchaseObjectArrayFieldValue = (value) => {
+ if (Array.isArray(value)) {
+ return value.filter(item => item && typeof item === 'object')
+ }
+ if (value && typeof value === 'object') {
+ return [value]
+ }
+ return []
+}
+
+const normalizePurchaseValueByFieldKey = (fieldKey, value) => {
+ if (fieldKey === 'attachmentMaterials') return normalizeAttachmentMaterialsValue(value)
+ if (purchaseIntegerFieldKeys.has(fieldKey)) return normalizePurchaseIntegerFieldValue(value)
+ if (purchaseDecimalFieldKeys.has(fieldKey)) return normalizePurchaseDecimalFieldValue(value)
+ if (purchaseBooleanFieldKeys.has(fieldKey)) return normalizePurchaseBooleanFieldValue(value)
+ if (purchaseStringArrayFieldKeys.has(fieldKey)) return normalizePurchaseStringArrayFieldValue(value)
+ if (purchaseObjectArrayFieldKeys.has(fieldKey)) return normalizePurchaseObjectArrayFieldValue(value)
+ if (purchaseStringFieldKeys.has(fieldKey)) return normalizePurchaseStringFieldValue(value)
+ return value
+}
+
+const normalizePurchasePayloadFieldTypes = (value, fieldKey = '') => {
+ if (purchaseGenericArrayFieldKeys.has(fieldKey)) {
+ if (Array.isArray(value)) {
+ return value.map(item => normalizePurchasePayloadFieldTypes(item))
+ }
+ if (value && typeof value === 'object') {
+ return [normalizePurchasePayloadFieldTypes(value)]
+ }
+ return []
+ }
+
+ if (purchaseStringArrayFieldKeys.has(fieldKey)) {
+ return normalizePurchaseStringArrayFieldValue(value)
+ }
+
+ if (purchaseObjectArrayFieldKeys.has(fieldKey)) {
+ return normalizePurchaseObjectArrayFieldValue(value)
+ }
+
+ if (Array.isArray(value)) {
+ return value.map(item => normalizePurchasePayloadFieldTypes(item))
+ }
+
+ if (value && typeof value === 'object') {
+ return Object.entries(value).reduce((result, [key, item]) => {
+ result[key] = normalizePurchasePayloadFieldTypes(item, key)
+ return result
+ }, {})
+ }
+
+ return normalizePurchaseValueByFieldKey(fieldKey, value)
+}
+
const sanitizePurchasePayloadForSubmit = (payload, businessType) => {
if (businessType !== 'purchase_ledger' || !payload || typeof payload !== 'object') return payload
- const sanitized = mergeLegacyProductDataIntoLedgers(Array.isArray(payload) ? [...payload] : { ...payload })
+ let sanitized = mergeLegacyProductDataIntoLedgers(Array.isArray(payload) ? [...payload] : { ...payload })
+ sanitized = normalizePurchaseAttachmentMaterialsField(sanitized)
+ sanitized = normalizePurchasePayloadFieldTypes(sanitized)
if (Array.isArray(sanitized.purchaseLedgers)) {
sanitized.purchaseLedgers = sanitized.purchaseLedgers.map(filterPurchaseLedgerRecord)
+ }
+ if (Array.isArray(sanitized.productData)) {
+ sanitized.productData = normalizeAndFilterPurchaseProductData(sanitized.productData)
+ }
+ if (Array.isArray(sanitized.productData) && !sanitized.productData.length) {
+ delete sanitized.productData
}
purchaseApprovalFieldKeys.forEach(key => {
@@ -1954,16 +2405,20 @@
return isLt10M
})
+ clearSelectedFiles()
selectedFiles.value = validFiles
uploadFileList.value = fileList.filter(item => item.raw && validFiles.includes(item.raw))
+ selectedFileSnapshots.value = validFiles.map((rawFile, index) => createLocalFileSnapshot(rawFile, index))
}
const removeSelectedFile = (index) => {
+ const [removedSnapshot] = selectedFileSnapshots.value.splice(index, 1)
+ revokeLocalFileSnapshots(removedSnapshot ? [removedSnapshot] : [])
selectedFiles.value.splice(index, 1)
uploadFileList.value.splice(index, 1)
}
-const analyzeFiles = async (files, message = '') => {
+const analyzeFiles = async (files, message = '', localFileSnapshots = []) => {
const uploadFiles = Array.isArray(files) ? files : [files].filter(Boolean)
if (!uploadFiles.length) return
if (isSending.value) return
@@ -1976,7 +2431,8 @@
isUser: true,
content: userMsg,
htmlContent: convertTextToHtml(userMsg),
- isTyping: false
+ isTyping: false,
+ localUploadFiles: Array.isArray(localFileSnapshots) ? localFileSnapshots : []
})
const botMsgIndex = messages.value.length
@@ -2075,9 +2531,9 @@
const msg = inputMessage.value?.trim() || ''
if ((msg || selectedFiles.value.length) && !isSending.value) {
if (selectedFiles.value.length) {
- analyzeFiles([...selectedFiles.value], msg)
- selectedFiles.value = []
- uploadFileList.value = []
+ const localFileSnapshots = selectedFileSnapshots.value
+ analyzeFiles([...selectedFiles.value], msg, localFileSnapshots)
+ clearSelectedFiles({ releaseSnapshots: false })
} else {
sendRequest(msg)
}
@@ -2659,8 +3115,9 @@
height: 100%;
}
:deep(.el-drawer__header) {
- margin-bottom: 0;
- padding: 0;
+ margin-bottom: 0 !important;
+ padding: 0 !important;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.12);
background: $gradient-dark;
color: #fff;
}
@@ -2671,7 +3128,7 @@
justify-content: space-between;
align-items: center;
width: 100%;
- padding: 18px 20px;
+ padding: 12px 18px;
background: $gradient-dark;
position: relative;
overflow: hidden;
@@ -2803,6 +3260,15 @@
opacity: 1;
}
}
+
+ :deep(.header-action-btn--text) {
+ width: auto !important;
+ min-width: 104px;
+ padding: 8px 14px !important;
+ font-size: 14px;
+ font-weight: 600;
+ white-space: nowrap;
+ }
}
.assistant-switcher {
@@ -2885,7 +3351,7 @@
top: 0;
left: 0;
right: 0;
- height: 240px;
+ height: 128px;
background: linear-gradient(180deg, rgba(0, 85, 212, 0.06) 0%, transparent 100%);
pointer-events: none;
}
@@ -3124,6 +3590,87 @@
background: rgba(0, 85, 212, 0.25);
border-radius: 2px;
}
+ }
+
+ .message-local-file-list {
+ margin-top: 8px;
+ display: grid;
+ gap: 8px;
+ max-width: 100%;
+ }
+
+ .message-local-file-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 8px 10px;
+ border-radius: 10px;
+ border: 1px solid rgba(88, 117, 255, 0.2);
+ background: rgba(255, 255, 255, 0.9);
+ max-width: 100%;
+
+ &.clickable {
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ border-color: rgba(44, 109, 255, 0.38);
+ background: rgba(243, 247, 255, 0.96);
+ }
+ }
+ }
+
+ .message-local-file-thumb {
+ width: 40px;
+ height: 40px;
+ border-radius: 6px;
+ overflow: hidden;
+ flex-shrink: 0;
+ border: 1px solid rgba(124, 148, 255, 0.26);
+ background: #f4f7ff;
+ cursor: zoom-in;
+
+ :deep(.el-image__inner) {
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ .message-local-file-icon {
+ font-size: 20px;
+ color: $primary-blue;
+ flex-shrink: 0;
+ }
+
+ .message-local-file-meta {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ }
+
+ .message-local-file-name {
+ font-size: 12px;
+ color: #1f2a44;
+ font-weight: 600;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &.clickable {
+ color: $primary-blue;
+ cursor: pointer;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+ }
+
+ .message-local-file-size {
+ font-size: 11px;
+ color: #7f8ba1;
+ line-height: 1.2;
}
}
@@ -3537,6 +4084,28 @@
font-size: 18px;
}
+ .selected-file-thumb {
+ width: 30px;
+ height: 30px;
+ border-radius: 6px;
+ overflow: hidden;
+ border: 1px solid rgba(0, 85, 212, 0.2);
+ flex-shrink: 0;
+ cursor: zoom-in;
+
+ :deep(.el-image__inner) {
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ .selected-file-meta {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ }
+
.file-name {
font-size: 13px;
color: $deep-blue;
@@ -3544,6 +4113,12 @@
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
+ }
+
+ .file-size {
+ font-size: 11px;
+ color: #5f86b4;
+ line-height: 1.1;
}
.remove-file {
@@ -3703,75 +4278,59 @@
.chat-hero {
display: grid;
- grid-template-columns: 164px minmax(0, 1fr);
- gap: 18px;
- align-items: start;
- padding: 14px 18px 6px;
+ grid-template-columns: 176px minmax(0, 1fr);
+ gap: 14px;
+ align-items: stretch;
+ padding: 8px 18px 4px;
&.compact {
- grid-template-columns: 122px minmax(0, 1fr);
- gap: 12px;
- padding: 8px 18px 2px;
+ grid-template-columns: 132px minmax(0, 1fr);
+ gap: 10px;
+ padding: 4px 18px 2px;
}
}
.assistant-stand {
position: relative;
- min-height: 252px;
+ min-height: 206px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
- padding-top: 18px;
+ padding-top: 8px;
overflow: hidden;
&.compact {
- min-height: 176px;
- padding-top: 8px;
+ min-height: 160px;
+ padding-top: 4px;
}
&.thinking {
.assistant-halo {
opacity: 1;
- transform: scale(1.08);
- filter: blur(8px);
+ transform: scale(1.12);
+ filter: blur(9px);
}
.assistant-scan-ring {
- opacity: 1;
- animation-duration: 1.6s;
+ opacity: 0.95;
+ animation-duration: 1.5s;
}
.assistant-orbit {
- opacity: 1;
+ opacity: 0.76;
}
- .assistant-bot {
- transform: translateY(-4px) scale(1.02);
+ .assistant-model-shell {
+ transform: translateY(-5px) scale(1.02);
}
- .assistant-bot-head {
- box-shadow: 0 0 30px rgba(80, 157, 255, 0.36);
+ .assistant-model-cut {
+ animation-duration: 2.2s;
}
- .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-model-img {
+ filter: saturate(1.06) drop-shadow(0 18px 20px rgba(22, 48, 80, 0.22));
}
.assistant-status {
@@ -3785,219 +4344,174 @@
animation: thinkingDot 1s ease-in-out infinite;
}
+ .assistant-base-lg {
+ animation-duration: 1.8s;
+ }
+
+ .assistant-base-md {
+ animation-duration: 1.5s;
+ }
+
.assistant-base-sm {
- box-shadow: 0 0 24px rgba(255, 93, 122, 0.48);
+ box-shadow: 0 0 24px rgba(30, 91, 255, 0.36);
+ animation-duration: 1.25s;
}
}
}
.assistant-halo {
position: absolute;
- top: 22px;
- width: 130px;
- height: 130px;
+ top: 24px;
+ width: 146px;
+ height: 146px;
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%);
+ background: radial-gradient(circle, rgba(31, 122, 114, 0.26) 0%, rgba(30, 91, 255, 0.2) 42%, rgba(109, 65, 237, 0.12) 66%, transparent 80%);
filter: blur(6px);
- opacity: 0.82;
+ opacity: 0.78;
transition: all 0.35s ease;
}
.assistant-scan-ring {
position: absolute;
- top: 40px;
- width: 132px;
- height: 132px;
+ top: 44px;
+ width: 136px;
+ height: 136px;
border-radius: 50%;
- border: 1px solid rgba(90, 159, 224, 0.22);
+ border: 1px solid rgba(67, 145, 223, 0.24);
box-shadow: inset 0 0 16px rgba(255, 255, 255, 0.25);
- opacity: 0.55;
+ opacity: 0.52;
animation: scanRing 4s linear infinite;
}
.assistant-orbit {
position: absolute;
top: 52px;
- width: 150px;
- height: 150px;
+ width: 156px;
+ height: 156px;
border-radius: 50%;
- border: 1px dashed rgba(92, 135, 255, 0.22);
- opacity: 0.45;
+ border: 1px dashed rgba(92, 135, 255, 0.24);
+ opacity: 0.42;
}
.assistant-orbit-a {
- animation: orbitRotate 8s linear infinite;
+ animation: orbitRotate 8.6s linear infinite;
}
.assistant-orbit-b {
- width: 118px;
- height: 118px;
+ width: 124px;
+ height: 124px;
top: 68px;
- border-color: rgba(255, 108, 150, 0.22);
- animation: orbitRotateReverse 5.6s linear infinite;
+ border-color: rgba(31, 122, 114, 0.24);
+ animation: orbitRotateReverse 6.2s linear infinite;
}
-.assistant-bot {
+.assistant-model-shell {
position: relative;
z-index: 1;
+ width: 148px;
+ height: 178px;
display: flex;
- flex-direction: column;
- align-items: center;
+ align-items: flex-end;
justify-content: center;
- margin-top: 12px;
+ margin-top: 4px;
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%;
+ bottom: 2px;
+ width: 164px;
+ height: 42px;
transform: translateX(-50%);
- background: linear-gradient(135deg, #54bfff, #7a41ff);
- box-shadow: 0 0 14px rgba(84, 191, 255, 0.65);
+ border-radius: 50%;
+ background: radial-gradient(
+ ellipse at center,
+ rgba(43, 126, 211, 0.32) 0%,
+ rgba(43, 126, 211, 0.14) 46%,
+ rgba(43, 126, 211, 0) 74%
+ );
+ filter: blur(2.6px);
+ animation: baseGlow 4.6s ease-in-out infinite;
+ z-index: 1;
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ left: 50%;
+ bottom: 10px;
+ width: 138px;
+ height: 28px;
+ transform: translateX(-50%);
+ border-radius: 50%;
+ border: 1px solid rgba(36, 116, 198, 0.6);
+ box-shadow:
+ inset 0 0 0 1px rgba(255, 255, 255, 0.58),
+ 0 0 22px rgba(42, 116, 196, 0.24);
+ animation: basePulse 3.2s ease-in-out infinite;
+ z-index: 4;
}
}
-.assistant-bot-antenna-left {
- left: 36px;
- transform: rotate(-14deg);
-}
-
-.assistant-bot-antenna-right {
- right: 36px;
- transform: rotate(14deg);
-}
-
-.assistant-bot-head {
+.assistant-model-cut {
position: relative;
+ width: 132px;
+ height: 178px;
+ z-index: 6;
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
+ transform-origin: center 84%;
+ animation: avatarFloat 3.2s ease-in-out infinite;
+}
+
+.assistant-model-img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ object-position: center bottom;
+ display: block;
+ filter: saturate(1.03) drop-shadow(0 14px 18px rgba(22, 49, 79, 0.2));
+ transition: filter 0.35s ease;
+}
+
+.assistant-model-fallback {
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);
+ height: 92px;
+ border-radius: 24px;
+ color: #fff;
+ background: linear-gradient(145deg, rgba(31, 122, 114, 0.9), rgba(30, 91, 255, 0.9));
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ box-shadow: 0 12px 24px rgba(31, 85, 173, 0.22);
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
-.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 {
+.assistant-base {
position: absolute;
left: 50%;
- bottom: 16px;
- width: 22px;
- height: 4px;
+ bottom: 8px;
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);
+ border: 1px solid rgba(36, 116, 198, 0.28);
+ background: radial-gradient(
+ ellipse at center,
+ rgba(255, 255, 255, 0.94) 0%,
+ rgba(81, 164, 233, 0.16) 58%,
+ rgba(30, 91, 255, 0.06) 100%
+ );
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2);
}
.assistant-status {
position: relative;
z-index: 1;
- margin-top: 14px;
- padding: 6px 12px;
+ margin-top: 7px;
+ padding: 5px 10px;
border-radius: 999px;
- font-size: 12px;
+ font-size: 11px;
font-weight: 600;
color: $deep-blue;
background: rgba(255, 255, 255, 0.95);
@@ -4017,81 +4531,25 @@
}
.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;
+ pointer-events: none;
}
.assistant-base-md {
- bottom: 6px;
- width: 88px;
- height: 20px;
- border-color: rgba(255, 93, 122, 0.34);
+ bottom: 15px;
+ width: 104px;
+ height: 22px;
+ border-color: rgba(36, 116, 198, 0.48);
+ animation: basePulse 2.8s ease-in-out infinite;
}
.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));
+ bottom: 20px;
+ width: 68px;
+ height: 14px;
+ background: linear-gradient(90deg, rgba(31, 122, 114, 0.82), rgba(45, 124, 255, 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);
- }
+ box-shadow: 0 0 18px rgba(45, 124, 255, 0.34);
+ animation: basePulse 2.2s ease-in-out infinite;
}
@keyframes orbitRotate {
@@ -4132,9 +4590,85 @@
}
}
+.assistant-base-lg {
+ width: 142px;
+ height: 32px;
+ animation: basePulse 3.4s ease-in-out infinite;
+
+ &::before {
+ content: '';
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ width: 130px;
+ height: 130px;
+ transform: translate(-50%, -50%);
+ border-radius: 50%;
+ background: conic-gradient(
+ from 180deg,
+ transparent 0deg,
+ rgba(36, 116, 198, 0.65) 48deg,
+ transparent 114deg,
+ rgba(36, 116, 198, 0.55) 212deg,
+ transparent 286deg,
+ rgba(31, 122, 114, 0.45) 334deg,
+ transparent 360deg
+ );
+ -webkit-mask: radial-gradient(circle, transparent 61%, #000 62%, #000 68%, transparent 70%);
+ mask: radial-gradient(circle, transparent 61%, #000 62%, #000 68%, transparent 70%);
+ opacity: 0.62;
+ animation: baseSpin 9s linear infinite;
+ }
+}
+
+@keyframes avatarFloat {
+ 0%,
+ 100% {
+ transform: translateY(0);
+ }
+ 50% {
+ transform: translateY(-7px);
+ }
+}
+
+@keyframes basePulse {
+ 0%,
+ 100% {
+ transform: translateX(-50%) scale(1);
+ opacity: 0.88;
+ }
+ 50% {
+ transform: translateX(-50%) scale(1.05);
+ opacity: 0.98;
+ }
+}
+
+@keyframes baseSpin {
+ from {
+ transform: translate(-50%, -50%) rotate(0deg);
+ }
+ to {
+ transform: translate(-50%, -50%) rotate(360deg);
+ }
+}
+
+@keyframes baseGlow {
+ 0%,
+ 100% {
+ transform: translateX(-50%) scaleX(1);
+ opacity: 0.82;
+ }
+ 50% {
+ transform: translateX(-50%) scaleX(1.06);
+ opacity: 0.96;
+ }
+}
+
.welcome-card {
position: relative;
- padding: 14px 14px 12px;
+ align-self: stretch;
+ min-height: 206px;
+ padding: 9px 10px 8px;
border-radius: 16px;
background:
linear-gradient(#fff, #fff) padding-box,
@@ -4143,7 +4677,8 @@
box-shadow: 0 16px 36px rgba(0, 85, 212, 0.12);
&.compact {
- padding: 10px 12px;
+ min-height: 160px;
+ padding: 8px 9px 7px;
border-radius: 12px;
box-shadow: 0 8px 16px rgba(0, 85, 212, 0.07);
@@ -4152,8 +4687,8 @@
}
.welcome-title {
- font-size: 17px;
- line-height: 1.3;
+ font-size: 16px;
+ line-height: 1.25;
br {
display: none;
@@ -4161,65 +4696,69 @@
}
.welcome-desc {
- margin-top: 6px;
- font-size: 12px;
- line-height: 1.55;
+ margin-top: 4px;
+ font-size: 11px;
+ line-height: 1.5;
}
.quick-prompt-list {
- margin-top: 10px;
- gap: 6px;
+ margin-top: 8px;
+ gap: 5px;
}
.quick-prompt-btn {
- padding: 8px 10px;
- font-size: 12px;
+ padding: 7px 9px;
+ font-size: 11px;
border-radius: 7px;
}
.more-prompts-btn {
- margin-top: 8px;
- font-size: 12px;
+ margin-top: 6px;
+ font-size: 11px;
}
}
}
.welcome-eyebrow {
- font-size: 11px;
+ font-size: 10px;
font-weight: 700;
letter-spacing: 2px;
color: rgba(0, 85, 212, 0.58);
- margin-bottom: 8px;
+ margin-bottom: 5px;
}
.welcome-title {
margin: 0;
- font-size: 26px;
- line-height: 1.2;
+ font-size: 20px;
+ line-height: 1.15;
font-weight: 800;
color: #172033;
+
+ br {
+ display: none;
+ }
}
.welcome-desc {
- margin: 10px 0 0;
- font-size: 13px;
- line-height: 1.7;
+ margin: 5px 0 0;
+ font-size: 12px;
+ line-height: 1.5;
color: #5f6980;
}
.quick-prompt-list {
display: grid;
- gap: 8px;
- margin-top: 14px;
+ gap: 6px;
+ margin-top: 8px;
}
.quick-prompt-btn {
width: 100%;
border: none;
- border-radius: 10px;
- padding: 11px 14px;
+ border-radius: 9px;
+ padding: 7px 10px;
text-align: left;
- font-size: 13px;
+ font-size: 12px;
font-weight: 600;
color: #fff;
cursor: pointer;
@@ -4267,14 +4806,14 @@
}
.more-prompts-btn {
- margin-top: 10px;
- padding: 0 12px;
- height: 32px;
+ margin-top: 6px;
+ padding: 0 10px;
+ height: 26px;
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-size: 12px;
font-weight: 600;
cursor: pointer;
display: inline-flex;
@@ -4289,21 +4828,6 @@
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));
}
}
@@ -4399,12 +4923,6 @@
.welcome-title {
font-size: 21px;
- }
-
- .hero-dot-grid {
- grid-template-columns: repeat(12, 1fr);
- gap: 6px;
- padding: 0 14px 12px;
}
.message-list {
--
Gitblit v1.9.3