| | |
| | | <div class="ai-chat-sidebar-wrapper"> |
| | | <!-- æ¬æµ®å¾æ --> |
| | | <div class="ai-chat-trigger" @click="toggleSidebar" v-show="!visible"> |
| | | <el-tooltip content="AI 婿" placement="left"> |
| | | <el-tooltip :content="currentAssistant.tooltip" placement="left"> |
| | | <div class="trigger-icon"> |
| | | <el-icon :size="30" color="#fff"><Cpu /></el-icon> |
| | | <el-icon :size="30" color="#fff"><component :is="currentAssistant.icon" /></el-icon> |
| | | </div> |
| | | </el-tooltip> |
| | | </div> |
| | |
| | | <template #header> |
| | | <div class="drawer-header"> |
| | | <div class="header-left"> |
| | | <el-icon :size="20" class="header-icon"><Cpu /></el-icon> |
| | | <span class="title">AI æºè½å©æ</span> |
| | | <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-icon><Delete /></el-icon> |
| | | </el-button> |
| | | </div> |
| | | <el-empty v-if="sessions.length === 0" description="ææ åå²ä¼è¯" /> |
| | | <el-empty v-if="sessions.length === 0" :description="currentAssistant.emptySessionText" /> |
| | | </div> |
| | | </el-skeleton> |
| | | </div> |
| | |
| | | <el-icon><VideoPause /></el-icon>åæ¢çæ |
| | | </el-button> |
| | | <el-upload |
| | | v-if="currentAssistant.allowFileUpload" |
| | | class="file-upload-trigger" |
| | | action="#" |
| | | :auto-upload="false" |
| | |
| | | v-model="inputMessage" |
| | | type="textarea" |
| | | :rows="selectedFile ? 2 : 3" |
| | | placeholder="请è¾å
¥æ¨çé®é¢... (Enter åé, Shift+Enter æ¢è¡)" |
| | | :placeholder="currentAssistant.placeholder" |
| | | resize="none" |
| | | @keydown.enter.exact.prevent="sendMessage" |
| | | /> |
| | |
| | | import { ref, onMounted, onUnmounted, nextTick, watch, computed } from 'vue' |
| | | import request from '@/utils/request' |
| | | import * as echarts from 'echarts' |
| | | import { Cpu, User, Plus, Loading, Timer, Delete, ChatDotSquare, VideoPause, Upload, Document, Close } from '@element-plus/icons-vue' |
| | | import { Cpu, User, Plus, Timer, Delete, ChatDotSquare, VideoPause, Upload, Document, Close, ShoppingCart } 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: 'ä½ å¥½', |
| | | allowFileUpload: true, |
| | | emptySessionText: 'ææ åå²ä¼è¯' |
| | | }, |
| | | { |
| | | key: 'purchase', |
| | | label: 'éè´å©ç', |
| | | title: 'éè´æºè½å©ç', |
| | | tooltip: 'éè´æºè½å©ç', |
| | | icon: ShoppingCart, |
| | | apiBase: '/purchase-ai', |
| | | storageKey: 'purchase_ai_chat_uuid', |
| | | placeholder: '请è¾å
¥éè´é®é¢... (Enter åé, Shift+Enter æ¢è¡)', |
| | | welcomeMessage: 'ä½ å¥½', |
| | | 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 visible = ref(false) |
| | | const windowWidth = ref(window.innerWidth) |
| | |
| | | const loadSessions = async () => { |
| | | loadingSessions.value = true |
| | | try { |
| | | const res = await request.get('/xiaozhi/history/sessions') |
| | | const res = await request.get(`${currentAssistant.value.apiBase}/history/sessions`) |
| | | if (res.code === 200) { |
| | | sessions.value = res.data || [] |
| | | } |
| | |
| | | const selectSession = async (session) => { |
| | | showHistory.value = false |
| | | uuid.value = session.memoryId |
| | | localStorage.setItem('ai_chat_uuid', uuid.value) |
| | | localStorage.setItem(currentAssistant.value.storageKey, uuid.value) |
| | | |
| | | // å è½½ä¼è¯æ¶æ¯ |
| | | try { |
| | | const res = await request.get(`/xiaozhi/history/messages/${uuid.value}`) |
| | | const res = await request.get(`${currentAssistant.value.apiBase}/history/messages/${uuid.value}`) |
| | | if (res.code === 200) { |
| | | disposeCharts() |
| | | messages.value = [] |
| | |
| | | |
| | | const handleDeleteSession = async (memoryId) => { |
| | | try { |
| | | const res = await request.delete(`/xiaozhi/history/${memoryId}`) |
| | | const res = await request.delete(`${currentAssistant.value.apiBase}/history/${memoryId}`) |
| | | if (res.code === 200) { |
| | | loadSessions() |
| | | if (uuid.value === memoryId) { |
| | |
| | | window.removeEventListener('resize', handleWindowResize) |
| | | }) |
| | | |
| | | watch(selectedAssistantKey, (nextKey, prevKey) => { |
| | | if (!prevKey || nextKey === prevKey) return |
| | | |
| | | if (currentAbortController.value) { |
| | | currentAbortController.value.abort() |
| | | currentAbortController.value = null |
| | | } |
| | | |
| | | isSending.value = false |
| | | disposeCharts() |
| | | messages.value = [] |
| | | outputState.value = {} |
| | | sessions.value = [] |
| | | showHistory.value = false |
| | | selectedFile.value = null |
| | | inputMessage.value = '' |
| | | initUUID() |
| | | hello() |
| | | }) |
| | | |
| | | const handleWindowResize = () => { |
| | | windowWidth.value = window.innerWidth |
| | | } |
| | |
| | | } |
| | | |
| | | const initUUID = () => { |
| | | let storedUUID = localStorage.getItem('ai_chat_uuid') |
| | | 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('ai_chat_uuid', storedUUID) |
| | | localStorage.setItem(currentAssistant.value.storageKey, storedUUID) |
| | | } |
| | | uuid.value = storedUUID |
| | | } |
| | | |
| | | const hello = () => { |
| | | sendRequest('ä½ å¥½') |
| | | sendRequest(currentAssistant.value.welcomeMessage || 'ä½ å¥½') |
| | | } |
| | | |
| | | const newChat = () => { |
| | | disposeCharts() |
| | | messages.value = [] |
| | | outputState.value = {} |
| | | localStorage.removeItem('ai_chat_uuid') |
| | | sessions.value = [] |
| | | showHistory.value = false |
| | | selectedFile.value = null |
| | | localStorage.removeItem(currentAssistant.value.storageKey) |
| | | initUUID() |
| | | hello() |
| | | } |
| | |
| | | formData.append('message', message.trim()) |
| | | } |
| | | |
| | | request.post('/xiaozhi/analyze-file', formData, { |
| | | request.post(`${currentAssistant.value.apiBase}/analyze-file`, formData, { |
| | | headers: { |
| | | 'Content-Type': 'multipart/form-data' |
| | | }, |
| | |
| | | |
| | | scrollToBottom() |
| | | |
| | | request.post('/xiaozhi/chat', |
| | | request.post(`${currentAssistant.value.apiBase}/chat`, |
| | | { memoryId: uuid.value, message }, |
| | | { |
| | | signal: currentAbortController.value.signal, |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | .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; |
| | | } |
| | | |
| | | :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 { |