refactor: 不知道改了什么

This commit is contained in:
Lexcubia 2025-04-29 16:34:08 +08:00
parent 8d75703422
commit 5a8c819c35
11 changed files with 209 additions and 235 deletions

View File

@ -21,6 +21,7 @@ alwaysApply: true
- src/
- assets/ # 静态资源文件
- components/ # 公共组件
- layouts/ # 布局组件
- views/ # 页面视图组件
- router/ # 路由配置
- store/ # Pinia状态管理
@ -44,6 +45,7 @@ alwaysApply: true
- 组件内使用 `<script setup>` 语法
- 遵循 Vue3 组合式 API 规范
- 使用 JavaScript 进行类型检查
- 保留必要的注释、console.log
3. 组件开发
- 组件必须包含 name 属性
@ -76,6 +78,7 @@ alwaysApply: true
- 遵循 BEM 命名规范
- 全局样式统一在 styles 目录下管理
- 组件样式使用 scoped
- 主题适配暗色和亮色
9. 提交规范
- 遵循 Conventional Commits 规范

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

Before

Width:  |  Height:  |  Size: 496 B

View File

@ -3,25 +3,17 @@
<h4>语音设置</h4>
<div class="settings-item">
<span class="settings-label">语音转文字后直接发送</span>
<el-switch
v-model="sendDirectly"
/>
<el-switch v-model="sendDirectly" />
</div>
<div class="settings-item">
<span class="settings-label">自动语音播报</span>
<el-switch
v-model="autoTTS"
/>
<el-switch v-model="autoTTS" />
</div>
<div class="settings-item">
<span class="settings-label">语音播报音色</span>
<el-select v-model="ttsVoice" placeholder="默认" style="width: 180px">
<el-option
v-for="voice in zhVoices"
:key="voice.name"
:label="voice.name + (voice.lang ? ' (' + voice.lang + ')' : '')"
:value="voice.name"
/>
<el-option v-for="voice in zhVoices" :key="voice.name"
:label="voice.name + (voice.lang ? ' (' + voice.lang + ')' : '')" :value="voice.name" />
<el-option label="默认" value="" />
</el-select>
</div>
@ -29,9 +21,9 @@
</template>
<script setup>
import { computed, ref, onMounted } from 'vue'
import { useSpeakStore } from '@/store/speak'
import { getTTSVoices } from '@/utils/speak'
import { computed, onMounted, ref } from 'vue'
const speakStore = useSpeakStore()
@ -50,9 +42,11 @@ const ttsVoice = computed({
})
const voices = ref([])
const zhVoices = computed(() =>
voices.value.filter(v => v.lang && v.lang.toLowerCase().startsWith('zh'))
)
const zhVoices = computed(() => {
const filteredVoices = voices.value.filter(v => v.lang && v.lang.toLowerCase().startsWith('zh'))
console.log('%c 可选语音:', 'color: #000; font-weight: bold', ...filteredVoices)
return filteredVoices
})
onMounted(async () => {
voices.value = await getTTSVoices()

View File

@ -1,23 +0,0 @@
{
"training": {
"name": "寿险代理人AI陪练",
"icon": "👨‍💼",
"token": "app-88ae2GN49aUyNO6qGg7tbTfX",
"background": "# 客户信息\n### **基础背景与性格设定** \n**姓名**:刘勇 \n**年龄**34 岁 \n**职业**:腾讯员工 \n**家庭**:妻子为全职太太,女儿 9 岁,双方父母健在 \n**性格**:冷静、理性、务实,偏好客观数据和逻辑分析,对保险持观望态度,防备心较强 \n**近期状态** \n- 体检显示中度脂肪肝,担心投保问题 \n- 已为全家配置百万医疗险,妻子有重疾险,但认为保障不足 (保费 50W\n- 对理财型保险收益不认可,但对补充重疾险有潜在需求 ",
"chatBackground": "通过尚先生介绍,你和他的老同学在他的家里首次面谈。他的家中布置简洁,茶几上摆放着一套茶具。"
},
"quote_objection": {
"name": "报价中异议",
"icon": "💬",
"token": "app-ur2Altw2LHR6niX8Q1S7Cn41",
"background": "# 客户信息\n### **基础背景与性格设定** \n**姓名**: 王大牛\n**年龄**: 40岁\n**职业**: 私营企业主\n**车牌号**: 闽C12345\n**车辆信息**拥有一辆行驶4年的大众途观L购买价格约25万主要用于商务出行及家庭使用。\n**投保信息**在其他保司购买交强险及商业险三者险、车损险还有1个月到期。有过一次轻微追尾事故已通过保司处理。王先生对成本控制非常敏感但也能意识到保险的重要性。他希望得到保障全面、价格合理的产品和优质的服务。",
"chatBackground": "现在你将扮演坐席专员,与系统扮演的客户针对报价中的各类异议开展对练,着重训练处理\"报价中异议\"的能力。按照\"保全保足\"原则给出险种推荐方案。"
},
"post_quote_objection": {
"name": "报价后异议",
"icon": "💰",
"token": "app-Yiccl0JoXs2QF2lkHxO6f822",
"background": "# 客户信息\n### **基础背景与性格设定** \n**姓名**: 张灵女士\n**年龄**: 32岁\n**职业**: 公司行政主管\n**车牌号**: 粤B56789\n**车辆信息**拥有一辆行驶4年多的本田思域1.5TCVT燃动版购买价格约15万用于日常通勤及周末短途出行。\n**投保信息**在其他保司购买交强险及商业险三者险、车损险、不计免赔险、车上人员责任险还有2个月到期。无事故记录仅有两次违章停车记录。张女士注重性价比和服务质量希望保险的保障全面、价格合理、服务贴心。",
"chatBackground": "现在你将扮演坐席专员,与系统扮演的客户针对报价后的各类异议开展对练,着重训练处理\"报价后异议\"的能力。按照\"保全保足\"原则给出险种推荐方案。"
}
}

View File

@ -1,4 +1,38 @@
import chatModes from '../data/chatModes.json'
const chatModes = {
"training": {
"name": "寿险代理人AI陪练",
"customerAvatar": "/src/assets/custom.png",
"userAvatar": "/src/assets/user.png",
"token": "app-88ae2GN49aUyNO6qGg7tbTfX",
"background": "# 客户信息\n### **基础背景与性格设定** \n**姓名**:刘勇 \n**年龄**34 岁 \n**职业**:腾讯员工 \n**家庭**:妻子为全职太太,女儿 9 岁,双方父母健在 \n**性格**:冷静、理性、务实,偏好客观数据和逻辑分析,对保险持观望态度,防备心较强 \n**近期状态** \n- 体检显示中度脂肪肝,担心投保问题 \n- 已为全家配置百万医疗险,妻子有重疾险,但认为保障不足 (保费 50W\n- 对理财型保险收益不认可,但对补充重疾险有潜在需求 ",
"chatBackground": "通过尚先生介绍,你和他的老同学在他的家里首次面谈。他的家中布置简洁,茶几上摆放着一套茶具。"
},
"ai_agent": {
"name": "AI寿险代理人",
"customerAvatar": "/src/assets/user.png",
"userAvatar": "/src/assets/custom.png",
"token": "app-eYoE51WXCaEKXvzb9ZCj6lBn",
"background": "# AI寿险代理人\n### **角色设定** \n**身份**AI寿险代理人 \n**特点**:专业、耐心、细致、富有同理心 \n**能力**\n- 精通各类寿险产品知识\n- 擅长需求分析和方案定制\n- 具备优秀的沟通和谈判技巧\n- 能够处理各类客户异议\n- 熟悉保险行业最新动态",
"chatBackground": "一位AI的保险代理人通过聊天来确认用户的需求收集用户的基本信息"
},
"quote_objection": {
"name": "报价中异议",
"customerAvatar": "/src/assets/custom.png",
"userAvatar": "/src/assets/user.png",
"token": "app-ur2Altw2LHR6niX8Q1S7Cn41",
"background": "# 客户信息\n### **基础背景与性格设定** \n**姓名**: 王大牛\n**年龄**: 40岁\n**职业**: 私营企业主\n**车牌号**: 闽C12345\n**车辆信息**拥有一辆行驶4年的大众途观L购买价格约25万主要用于商务出行及家庭使用。\n**投保信息**在其他保司购买交强险及商业险三者险、车损险还有1个月到期。有过一次轻微追尾事故已通过保司处理。王先生对成本控制非常敏感但也能意识到保险的重要性。他希望得到保障全面、价格合理的产品和优质的服务。",
"chatBackground": "现在你将扮演坐席专员,与系统扮演的客户针对报价中的各类异议开展对练,着重训练处理\"报价中异议\"的能力。按照\"保全保足\"原则给出险种推荐方案。"
},
"post_quote_objection": {
"name": "报价后异议",
"customerAvatar": "/src/assets/custom.png",
"userAvatar": "/src/assets/user.png",
"token": "app-Yiccl0JoXs2QF2lkHxO6f822",
"background": "# 客户信息\n### **基础背景与性格设定** \n**姓名**: 张灵女士\n**年龄**: 32岁\n**职业**: 公司行政主管\n**车牌号**: 粤B56789\n**车辆信息**拥有一辆行驶4年多的本田思域1.5TCVT燃动版购买价格约15万用于日常通勤及周末短途出行。\n**投保信息**在其他保司购买交强险及商业险三者险、车损险、不计免赔险、车上人员责任险还有2个月到期。无事故记录仅有两次违章停车记录。张女士注重性价比和服务质量希望保险的保障全面、价格合理、服务贴心。",
"chatBackground": "现在你将扮演坐席专员,与系统扮演的客户针对报价后的各类异议开展对练,着重训练处理\"报价后异议\"的能力。按照\"保全保足\"原则给出险种推荐方案。"
}
}
export default [
// 获取聊天模式配置
{

View File

@ -31,7 +31,7 @@ const routes = [
icon: Monitor,
title: "API测试"
},
component: () => import("@/views/chat/test/TestApi.vue"),
component: () => import("@/views/test/TestApi.vue"),
},
];

View File

@ -1,65 +1,9 @@
import { defineStore } from 'pinia'
import { sendMessage } from '@/api/chat'
import { useSettingsStore } from '@/store/settings'
import { speakText } from '@/utils/speak'
import { getChatModes, sendMessage } from '@/api/chat'
import { useSpeakStore } from '@/store/speak'
import { speakText } from '@/utils/speak'
import { defineStore } from 'pinia'
const chatModes = {
training: {
name: '寿险代理人AI陪练',
icon: '👨‍💼',
token: 'app-88ae2GN49aUyNO6qGg7tbTfX',
background: `# 客户信息
### **基础背景与性格设定**
**姓名**刘勇
**年龄**34
**职业**腾讯员工
**家庭**妻子为全职太太女儿 9 双方父母健在
**性格**冷静理性务实偏好客观数据和逻辑分析对保险持观望态度防备心较强
**近期状态**
- 体检显示中度脂肪肝担心投保问题
- 已为全家配置百万医疗险妻子有重疾险但认为保障不足 保费 50W
- 对理财型保险收益不认可但对补充重疾险有潜在需求 `,
chatBackground: '通过尚先生介绍,你和他的老同学在他的家里首次面谈。他的家中布置简洁,茶几上摆放着一套茶具。',
},
quote_objection: {
name: '报价中异议',
icon: '💬',
token: 'app-ur2Altw2LHR6niX8Q1S7Cn41',
background: `# 客户信息
### **基础背景与性格设定**
**姓名**: 王大牛
**年龄**: 40
**职业**: 私营企业主
**车牌号**: 闽C12345
**车辆信息**拥有一辆行驶4年的大众途观L购买价格约25万主要用于商务出行及家庭使用
**投保信息**在其他保司购买交强险及商业险三者险车损险还有1个月到期有过一次轻微追尾事故已通过保司处理王先生对成本控制非常敏感但也能意识到保险的重要性他希望得到保障全面价格合理的产品和优质的服务`,
chatBackground: '现在你将扮演坐席专员,与系统扮演的客户针对报价中的各类异议开展对练,着重训练处理"报价中异议"的能力。按照"保全保足"原则给出险种推荐方案。',
},
post_quote_objection: {
name: '报价后异议',
icon: '💰',
token: 'app-Yiccl0JoXs2QF2lkHxO6f822',
background: `# 客户信息
### **基础背景与性格设定**
**姓名**: 张灵女士
**年龄**: 32
**职业**: 公司行政主管
**车牌号**: 粤B56789
**车辆信息**拥有一辆行驶4年多的本田思域1.5TCVT燃动版购买价格约15万用于日常通勤及周末短途出行
**投保信息**在其他保司购买交强险及商业险三者险车损险不计免赔险车上人员责任险还有2个月到期无事故记录仅有两次违章停车记录张女士注重性价比和服务质量希望保险的保障全面价格合理服务贴心`,
chatBackground: '现在你将扮演坐席专员,与系统扮演的客户针对报价后的各类异议开展对练,着重训练处理"报价后异议"的能力。按照"保全保足"原则给出险种推荐方案。',
}
}
// 返回示例
/*
data: {"event": "message", "conversation_id": "fac9e3e7-2b2e-4fc2-bf19-0a6d4cfcf529", "message_id": "e8f42230-205c-4f31-b068-86b37a18c2ee", "created_at": 1744881805, "task_id": "ec521a05-6068-427a-ac5d-d349f0d05874", "id": "e8f42230-205c-4f31-b068-86b37a18c2ee", "answer": "\u7684\u65b9\u5411", "from_variable_selector": ["17441814078920", "text"]}
data: {"event": "message", "conversation_id": "fac9e3e7-2b2e-4fc2-bf19-0a6d4cfcf529", "message_id": "e8f42230-205c-4f31-b068-86b37a18c2ee", "created_at": 1744881805, "task_id": "ec521a05-6068-427a-ac5d-d349f0d05874", "id": "e8f42230-205c-4f31-b068-86b37a18c2ee", "answer": "\u3002", "from_variable_selector": ["17441814078920", "text"]}
data: {"event": "message_end", "conversation_id": "fac9e3e7-2b2e-4fc2-bf19-0a6d4cfcf529", "message_id": "e8f42230-205c-4f31-b068-86b37a18c2ee", "created_at": 1744881805, "task_id": "ec521a05-6068-427a-ac5d-d349f0d05874", "id": "e8f42230-205c-4f31-b068-86b37a18c2ee", "metadata": {"usage": {"prompt_tokens": 2483, "prompt_unit_price": "4.13", "prompt_price_unit": "0.000001", "prompt_price": "0.0102548", "completion_tokens": 142, "completion_unit_price": "4.13", "completion_price_unit": "0.000001", "completion_price": "0.0005865", "total_tokens": 2625, "total_price": "0.0108413", "currency": "RMB", "latency": 13.045417829882354}}, "files": []}
*/
const tagList = ['kehu', 'pingfen', 'zongjie', 'dafen']
const tagList = ['kehu', 'pingfen', 'zongjie', 'huaxiang', 'dafen']
// 处理标签内容的辅助函数
const processTagContent = (answerCache, tagName, cachedMessage) => {
@ -111,11 +55,12 @@ export const useChatStore = defineStore('chat', {
currentMode: 'training',
conversationId: null,
messageCache: {},
chatModes: {},
chatModesLoading: false,
}),
getters: {
chatModes: () => chatModes,
currentToken: (state) => chatModes[state.currentMode].token,
currentToken: (state) => state.chatModes[state.currentMode]?.token,
currentConversation: (state) => {
return state.conversationId ? state.messages[state.conversationId] : null
},
@ -150,6 +95,19 @@ export const useChatStore = defineStore('chat', {
},
actions: {
async fetchChatModes() {
if (this.chatModesLoading) return
this.chatModesLoading = true
try {
const response = await getChatModes()
this.chatModes = response.data
} catch (error) {
console.error('获取聊天模式失败:', error)
} finally {
this.chatModesLoading = false
}
},
setCurrentMode(mode) {
this.currentMode = mode
@ -223,10 +181,12 @@ export const useChatStore = defineStore('chat', {
kehu: '',
pingfen: '',
zongjie: '',
huaxiang: '',
dafen: '',
kehuFinished: false,
pingfenFinished: false,
zongjieFinished: false,
huaxiangFinished: false,
dafenFinished: false,
}
this.conversationId = message.conversation_id
@ -359,7 +319,7 @@ export const useChatStore = defineStore('chat', {
persist: {
key: 'chatStore',
storage: sessionStorage,
paths: ['messages', 'currentMode', 'conversationId']
storage: localStorage,
paths: ['messages', 'currentMode', 'conversationId', 'chatModes']
}
})

View File

@ -1,5 +1,4 @@
import { defineStore } from 'pinia'
import Speech from 'speak-tts'
export const useSpeakStore = defineStore('speak', {
state: () => ({
@ -16,6 +15,7 @@ export const useSpeakStore = defineStore('speak', {
this.ttsStatus = status
},
setTTSVoice(voice) {
console.log('%c 当前语音:', 'color: green; font-weight: bold', voice)
this.ttsVoice = voice
},
setSendDirectly(val) {

View File

@ -19,7 +19,7 @@
<!-- 用户消息 -->
<div class="message">
<div class="message-wrapper user">
<el-avatar :src="userAvatarUrl" :size="40" class="avatar" />
<el-avatar :src="userAvatar" :size="40" class="avatar" />
<div class="message-content">
<div class="message-text-wrapper">
<div v-if="message.createdAt" class="message-timestamp">
@ -33,13 +33,16 @@
@toggle="chatStore.toggleEvaluation(message.messageId)" />
</div>
<!-- 评价总结内容 -->
<div v-if="message.showEvaluation && (message.pingfen || message.zongjie)" class="evaluation-section">
<div v-if="message.showEvaluation && (message.pingfen || message.zongjie || message.huaxiang)"
class="evaluation-section">
<el-button class="evaluation-close-btn" type="info" size="small" circle :icon="Close"
@click="chatStore.toggleEvaluation(message.messageId)" />
<div class="evaluation-content">
<div class="evaluation-text pingfen" v-html="formatMarkdown(message.pingfen, 'pingfen')"></div>
<div class="evaluation-text zongjie" v-html="formatMarkdown(message.zongjie, 'zongjie')"></div>
<div v-if="['pingfen', 'zongjie'].includes(message?.respondingType)" class="typing-indicator">
<div class="evaluation-text huaxiang" v-html="formatMarkdown(message.huaxiang, 'huaxiang')"></div>
<div v-if="['pingfen', 'zongjie', 'huaxiang'].includes(message?.respondingType)"
class="typing-indicator">
<span></span><span></span><span></span>
</div>
</div>
@ -50,7 +53,7 @@
<!-- 回答 -->
<div class="message">
<div class="message-wrapper assistant">
<el-avatar :src="customAvatarUrl" :size="40" class="avatar" />
<el-avatar :src="customerAvatar" :size="40" class="avatar" />
<div class="message-content">
<div class="message-text-wrapper">
<div v-if="message.createdAt" class="message-timestamp">
@ -97,8 +100,6 @@
</template>
<script setup>
import customAvatarUrl from '@/assets/custom.png'
import userAvatarUrl from '@/assets/user.png'
import { useChatStore } from '@/store/chat'
import ChatHeader from '@/views/chat/components/ChatHeader.vue'
import ChatInput from '@/views/chat/components/ChatInput.vue'
@ -152,6 +153,15 @@ const lastMessageWithDafen = computed(() => {
return null
})
//
const customerAvatar = computed(() => {
return chatStore.chatModes[chatStore.currentMode]?.customerAvatar
})
const userAvatar = computed(() => {
return chatStore.chatModes[chatStore.currentMode]?.userAvatar
})
// marked
marked.setOptions({
breaks: true,
@ -219,7 +229,7 @@ watch(() => currentMessages.value, (newMessages, oldMessages) => {
//
const lastMessage = newMessages[newMessages.length - 1]
if (lastMessage && ['kehu', 'pingfen', 'zongjie'].includes(lastMessage.respondingType)) {
if (lastMessage && ['kehu', 'pingfen', 'zongjie', 'huaxiang'].includes(lastMessage.respondingType)) {
scrollToBottom(true) //
}
}, { deep: true })
@ -228,7 +238,7 @@ watch(() => currentMessages.value, (newMessages, oldMessages) => {
watch(() => currentMessages.value?.map(msg => msg.respondingType), (newTypes, oldTypes) => {
if (!newTypes || !oldTypes) return
const lastType = newTypes[newTypes.length - 1]
if (['kehu', 'pingfen', 'zongjie'].includes(lastType)) {
if (['kehu', 'pingfen', 'zongjie', 'huaxiang'].includes(lastType)) {
scrollToBottom(true) //
}
}, { deep: true })

View File

@ -1,51 +1,51 @@
<template>
<div class="mode-selector-container">
<div
class="mode-selector-backdrop"
:class="{ visible: !settingsStore.sidebarCollapsed }"
@click="settingsStore.toggleSidebar"
v-if="isMobile"
></div>
<div class="mode-selector-backdrop" :class="{ visible: !settingsStore.sidebarCollapsed }"
@click="settingsStore.toggleSidebar" v-if="isMobile"></div>
<div class="mode-selector" :class="{ 'collapsed': settingsStore.sidebarCollapsed }">
<div class="mode-header">
<!-- Expanded Logo -->
<div class="mode-header-title" v-if="!settingsStore.sidebarCollapsed">
<img src="/lgogo.svg" alt="Logo" class="mode-header-icon" />
<img src="/lgogo.svg" alt="Logo" class="mode-header-icon" />
</div>
<!-- Collapsed Logo -->
<div class="mode-header-collapsed-logo" v-if="settingsStore.sidebarCollapsed">
<img src="/logo.svg" alt="Logo" class="mode-header-icon collapsed" />
<img src="/logo.svg" alt="Logo" class="mode-header-icon collapsed" />
</div>
</div>
<el-menu
:default-active="chatStore.currentMode"
class="mode-menu"
:collapse="settingsStore.sidebarCollapsed"
@select="selectMode"
>
<el-menu :default-active="chatStore.currentMode" class="mode-menu" :collapse="settingsStore.sidebarCollapsed"
@select="selectMode">
<el-menu-item class="mode-item new-chat-item" @click="handleNewChat">
<el-icon><Plus /></el-icon>
<el-icon>
<Plus />
</el-icon>
<span class="mode-name">新建会话</span>
</el-menu-item>
<el-menu-item v-for="mode in chatModes" :key="mode.id" :index="mode.id" class="mode-item">
<el-icon><component :is="mode.icon" /></el-icon>
<span class="mode-name">{{ mode.name }}</span>
</el-menu-item>
<!-- 加载状态 -->
<div v-if="chatStore.chatModesLoading" class="loading-container">
<el-skeleton :rows="3" animated />
</div>
<!-- 正常状态 -->
<template v-else>
<el-menu-item v-for="mode in chatModes" :key="mode.id" :index="mode.id" class="mode-item">
<el-icon>
<component :is="mode.icon" />
</el-icon>
<span class="mode-name">{{ mode.name }}</span>
</el-menu-item>
</template>
</el-menu>
<ChatHistoryList />
<!-- Settings Button Popover -->
<el-popover
placement="top-end"
trigger="click"
popper-class="settings-popover-popper"
>
<el-popover placement="top-end" trigger="click" popper-class="settings-popover-popper">
<template #reference>
<div class="sidebar-footer">
<div class="settings-button" title="设置">
<el-icon><Setting /></el-icon>
<el-icon>
<Setting />
</el-icon>
<span v-if="!settingsStore.sidebarCollapsed" class="button-text">设置</span>
</div>
</div>
@ -59,57 +59,37 @@
</template>
<script setup>
import SettingsPanel from '@/components/settings/SettingsPanel.vue'
import { useChatStore } from '@/store/chat'
import { useSettingsStore } from '@/store/settings'
import { computed, ref, onMounted, onUnmounted } from 'vue'
import { ChatDotRound, ChatLineRound, ChatRound, Service, Setting, Clock, Delete, Plus } from '@element-plus/icons-vue'
import { ElMenu, ElMenuItem, ElIcon, ElPopover, ElMessageBox } from 'element-plus'
import SettingsPanel from '@/components/settings/SettingsPanel.vue'
import ChatHistoryList from '@/views/chat/components/ChatHistoryList.vue'
import { ChatDotRound, ChatLineRound, ChatRound, Plus, Service, Setting } from '@element-plus/icons-vue'
import { ElIcon, ElMenu, ElMenuItem, ElPopover, ElSkeleton } from 'element-plus'
import { computed, onMounted, onUnmounted, ref } from 'vue'
const chatStore = useChatStore()
const settingsStore = useSettingsStore()
//
const loadChatModes = async () => {
try {
await chatStore.fetchChatModes()
} catch (error) {
console.error('加载聊天模式失败:', error)
}
}
const chatModes = computed(() => {
console.log(chatStore, 'chatModes')
return Object.keys(chatStore.chatModes).map(key => ({
id: key,
name: chatStore.chatModes[key].name,
icon: key === 'general' ? ChatRound :
key === 'assistant' ? Service :
key === 'chat' ? ChatDotRound : ChatLineRound
key === 'assistant' ? Service :
key === 'chat' ? ChatDotRound : ChatLineRound
}))
})
//
const currentModeHistory = computed(() => {
return chatStore.conversations.filter(chat => chat.mode === chatStore.currentMode)
})
const formatChatTitle = (chat) => {
if (chat.messages && chat.messages.length > 0) {
const firstMessage = chat.messages[0].question
return firstMessage.length > 20 ? firstMessage.substring(0, 20) + '...' : firstMessage
}
return `会话 ${chat.conversationId.substring(0, 8)}`
}
const handleDelete = async (conversationId) => {
try {
await ElMessageBox.confirm(
'确定要删除这个会话吗?删除后无法恢复。',
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
confirmButtonClass: 'el-button--danger'
}
)
chatStore.deleteConversation(conversationId)
} catch {
//
}
}
const selectMode = (id) => {
// ID
@ -134,6 +114,8 @@ const checkMobile = () => {
onMounted(() => {
checkMobile()
window.addEventListener('resize', checkMobile)
//
loadChatModes()
})
onUnmounted(() => {
@ -248,7 +230,7 @@ onUnmounted(() => {
transform: translateX(0);
body:not(.no-animations) & {
transition: opacity 0.3s ease-in-out;
transition: opacity 0.3s ease-in-out;
}
}
}
@ -299,6 +281,7 @@ onUnmounted(() => {
.settings-button {
width: 40px;
height: 40px;
.mode-name,
.button-text {
opacity: 0;
@ -390,9 +373,9 @@ onUnmounted(() => {
}
.mode-header-icon.collapsed {
height: 32px;
filter: drop-shadow(0 0 0.75em rgba(var(--el-color-primary-rgb), 0.6));
vertical-align: middle;
height: 32px;
filter: drop-shadow(0 0 0.75em rgba(var(--el-color-primary-rgb), 0.6));
vertical-align: middle;
}
}
@ -421,6 +404,7 @@ onUnmounted(() => {
.settings-button {
width: 40px;
height: 40px;
.mode-name,
.button-text {
opacity: 0;
@ -480,6 +464,19 @@ onUnmounted(() => {
opacity: 0.6;
cursor: not-allowed;
}
.loading-container {
padding: 12px;
}
.error-container {
padding: 20px;
text-align: center;
.el-button {
margin-top: 12px;
}
}
</style>
<style lang="scss">
@ -495,13 +492,13 @@ onUnmounted(() => {
<style lang="scss">
/* Dark mode override */
html.dark .mode-selector {
background-color: var(--el-color-primary-dark-2);
border-right-color: var(--el-border-color-dark);
background-color: var(--el-color-primary-dark-2);
border-right-color: var(--el-border-color-dark);
/* Dark mode hover */
.mode-menu :deep(.el-menu-item):not(.is-active):hover {
background-color: var(--el-fill-color);
color: var(--el-color-primary);
background-color: var(--el-fill-color);
color: var(--el-color-primary);
}
}
</style>