refactor: 不知道改了什么
This commit is contained in:
parent
8d75703422
commit
5a8c819c35
|
@ -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 规范
|
||||
|
|
|
@ -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 |
|
@ -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()
|
||||
|
@ -66,11 +60,11 @@ onMounted(async () => {
|
|||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
|
||||
|
||||
.settings-label {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
@ -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.5T,CVT燃动版,购买价格约15万,用于日常通勤及周末短途出行。\n**投保信息**:在其他保司购买交强险及商业险(三者险、车损险、不计免赔险、车上人员责任险),还有2个月到期。无事故记录,仅有两次违章停车记录。张女士注重性价比和服务质量,希望保险的保障全面、价格合理、服务贴心。",
|
||||
"chatBackground": "现在你将扮演坐席专员,与系统扮演的客户针对报价后的各类异议开展对练,着重训练处理\"报价后异议\"的能力。按照\"保全保足\"原则给出险种推荐方案。"
|
||||
}
|
||||
}
|
|
@ -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.5T,CVT燃动版,购买价格约15万,用于日常通勤及周末短途出行。\n**投保信息**:在其他保司购买交强险及商业险(三者险、车损险、不计免赔险、车上人员责任险),还有2个月到期。无事故记录,仅有两次违章停车记录。张女士注重性价比和服务质量,希望保险的保障全面、价格合理、服务贴心。",
|
||||
"chatBackground": "现在你将扮演坐席专员,与系统扮演的客户针对报价后的各类异议开展对练,着重训练处理\"报价后异议\"的能力。按照\"保全保足\"原则给出险种推荐方案。"
|
||||
}
|
||||
}
|
||||
|
||||
export default [
|
||||
// 获取聊天模式配置
|
||||
{
|
||||
|
|
|
@ -31,7 +31,7 @@ const routes = [
|
|||
icon: Monitor,
|
||||
title: "API测试"
|
||||
},
|
||||
component: () => import("@/views/chat/test/TestApi.vue"),
|
||||
component: () => import("@/views/test/TestApi.vue"),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -1,72 +1,16 @@
|
|||
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.5T,CVT燃动版,购买价格约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) => {
|
||||
// 检查是否包含完整的开始标签
|
||||
const startTag = `<${tagName}>`
|
||||
const endTag = `</${tagName}>`
|
||||
|
||||
|
||||
// 如果正在处理该标签
|
||||
if (cachedMessage.respondingType === tagName) {
|
||||
// 检查是否包含结束标签
|
||||
|
@ -83,12 +27,12 @@ const processTagContent = (answerCache, tagName, cachedMessage) => {
|
|||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 检查是否包含开始标签
|
||||
if (answerCache.includes(startTag)) {
|
||||
cachedMessage.respondingType = tagName
|
||||
const tagContent = answerCache.split(startTag)[1] || ''
|
||||
|
||||
|
||||
// 检查开始标签后是否立即包含结束标签
|
||||
if (tagContent.includes(endTag)) {
|
||||
const content = tagContent.split(endTag)[0]
|
||||
|
@ -101,7 +45,7 @@ const processTagContent = (answerCache, tagName, cachedMessage) => {
|
|||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return answerCache
|
||||
}
|
||||
|
||||
|
@ -111,26 +55,27 @@ 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
|
||||
},
|
||||
currentMessages: (state) => {
|
||||
// 根据conversationId和currentMode找到对应的messageCache的messageId
|
||||
const cache = Object.values(state.messageCache).find(message =>
|
||||
const cache = Object.values(state.messageCache).find(message =>
|
||||
message.mode === state.currentMode && message.conversationId === state.conversationId
|
||||
)
|
||||
|
||||
|
||||
// 获取当前会话的消息
|
||||
const currentConversation = state.conversationId ? state.messages[state.conversationId] : null
|
||||
const conversationMessages = currentConversation ? currentConversation.chatMessages : []
|
||||
|
||||
return conversationMessages.filter(message =>
|
||||
message.mode === state.currentMode &&
|
||||
|
||||
return conversationMessages.filter(message =>
|
||||
message.mode === state.currentMode &&
|
||||
(message.conversationId === state.conversationId || !state.conversationId) &&
|
||||
(!cache || message.messageId !== cache.messageId)
|
||||
).concat(cache ? [cache] : [])
|
||||
|
@ -150,17 +95,30 @@ 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
|
||||
|
||||
|
||||
// 找到当前模式的最后一条会话
|
||||
const lastConversation = this.conversations
|
||||
.filter(conv => conv.mode === mode)
|
||||
.sort((a, b) => b.createdAt - a.createdAt)[0]
|
||||
|
||||
|
||||
// 设置当前会话ID
|
||||
this.conversationId = lastConversation?.conversationId || null
|
||||
|
||||
|
||||
console.log('%c currentConversation:', 'color: #2196F3; font-weight: bold', {
|
||||
mode,
|
||||
lastConversation,
|
||||
|
@ -183,14 +141,14 @@ export const useChatStore = defineStore('chat', {
|
|||
|
||||
async sendMessage(question) {
|
||||
if (!question.trim()) return
|
||||
|
||||
|
||||
// 设置会话状态为正在输入
|
||||
if (this.conversationId && this.messages[this.conversationId]) {
|
||||
this.messages[this.conversationId].conversationStatus = 'typing'
|
||||
}
|
||||
|
||||
|
||||
this.hasStartedResponse = false
|
||||
|
||||
|
||||
const spokenSet = new Set(); // 新增:记录已播报的 messageId
|
||||
try {
|
||||
const messageStream = await sendMessage({
|
||||
|
@ -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
|
||||
|
@ -243,7 +203,7 @@ export const useChatStore = defineStore('chat', {
|
|||
}
|
||||
}
|
||||
break
|
||||
case 'message':
|
||||
case 'message':
|
||||
if (message.answer) {
|
||||
console.log('%c message:answer:', 'color: #2196F3; font-weight: bold', message.answer)
|
||||
answerCache += message.answer
|
||||
|
@ -273,10 +233,10 @@ export const useChatStore = defineStore('chat', {
|
|||
if (cachedMessage) {
|
||||
cachedMessage.respondingType = ''
|
||||
cachedMessage.answerBuffer = message.data.outputs.answer
|
||||
|
||||
|
||||
|
||||
|
||||
this.messages[cachedMessage.conversationId].chatMessages.push(cachedMessage)
|
||||
|
||||
|
||||
// 如果包含dafen标签,将会话标记为完成并将打分内容归档到summary
|
||||
if (cachedMessage.dafen) {
|
||||
this.messages[cachedMessage.conversationId].conversationStatus = 'finished'
|
||||
|
@ -285,7 +245,7 @@ export const useChatStore = defineStore('chat', {
|
|||
// 恢复会话状态为活跃
|
||||
this.messages[cachedMessage.conversationId].conversationStatus = 'active'
|
||||
}
|
||||
|
||||
|
||||
console.log('%c workflow_finished:cachedMessage:', 'color: #ff0000; font-weight: bold', cachedMessage)
|
||||
}
|
||||
delete this.messageCache[message.message_id]
|
||||
|
@ -300,7 +260,7 @@ export const useChatStore = defineStore('chat', {
|
|||
},
|
||||
toggleEvaluation(messageId) {
|
||||
console.log('%c toggleEvaluation:messageId:', 'color: #2196F3; font-weight: bold', messageId)
|
||||
const cache = Object.values(this.messageCache).find(message =>
|
||||
const cache = Object.values(this.messageCache).find(message =>
|
||||
message.mode === this.currentMode && message.messageId === messageId
|
||||
)
|
||||
if (cache) {
|
||||
|
@ -323,7 +283,7 @@ export const useChatStore = defineStore('chat', {
|
|||
// if (this.conversationId && this.messages[this.conversationId]) {
|
||||
// this.messages[this.conversationId].conversationStatus = 'finished'
|
||||
// }
|
||||
|
||||
|
||||
// 只清除当前会话相关的消息缓存
|
||||
if (this.conversationId) {
|
||||
// 从messageCache中删除当前会话的消息
|
||||
|
@ -333,7 +293,7 @@ export const useChatStore = defineStore('chat', {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 清除当前会话ID
|
||||
this.conversationId = null
|
||||
},
|
||||
|
@ -349,7 +309,7 @@ export const useChatStore = defineStore('chat', {
|
|||
})
|
||||
this.conversationId = null
|
||||
}
|
||||
|
||||
|
||||
// 从messages中删除会话
|
||||
if (this.messages[conversationId]) {
|
||||
delete this.messages[conversationId]
|
||||
|
@ -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']
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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) {
|
||||
|
@ -30,4 +30,4 @@ export const useSpeakStore = defineStore('speak', {
|
|||
storage: localStorage,
|
||||
paths: ['ttsVoice', 'sendDirectly', 'autoTTS']
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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
|
||||
icon: key === 'general' ? ChatRound :
|
||||
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(() => {
|
||||
|
@ -160,7 +142,7 @@ onUnmounted(() => {
|
|||
body:not(.no-animations) & {
|
||||
transition: width 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
|
||||
&.collapsed {
|
||||
width: 64px;
|
||||
}
|
||||
|
@ -172,21 +154,21 @@ onUnmounted(() => {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
|
||||
&-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
|
||||
.mode-header-icon {
|
||||
height: 32px;
|
||||
will-change: filter;
|
||||
|
||||
|
||||
body:not(.no-animations) & {
|
||||
transition: filter 300ms;
|
||||
}
|
||||
|
||||
|
||||
&:hover {
|
||||
filter: drop-shadow(0 0 0.75em rgba(var(--el-color-primary-rgb), 0.6));
|
||||
filter: drop-shadow(0 0 0.75em rgba(var(--el-color-primary-rgb), 0.6));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -224,7 +206,7 @@ onUnmounted(() => {
|
|||
font-size: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
|
||||
&.is-active {
|
||||
background-image: linear-gradient(135deg, var(--el-color-primary) 0%, var(--el-color-primary-light-3) 100%);
|
||||
color: var(--el-color-white);
|
||||
|
@ -234,21 +216,21 @@ onUnmounted(() => {
|
|||
color: var(--el-color-white);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&:not(.is-active):hover {
|
||||
background-color: var(--el-fill-color-light);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
|
||||
.mode-name {
|
||||
margin-left: 12px;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
|
||||
|
||||
body:not(.no-animations) & {
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -260,7 +242,7 @@ onUnmounted(() => {
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 60px;
|
||||
|
||||
|
||||
.mode-header-collapsed-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -273,7 +255,7 @@ onUnmounted(() => {
|
|||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.mode-menu {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
@ -299,6 +281,7 @@ onUnmounted(() => {
|
|||
.settings-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
.mode-name,
|
||||
.button-text {
|
||||
opacity: 0;
|
||||
|
@ -358,17 +341,17 @@ onUnmounted(() => {
|
|||
background-color: var(--el-overlay-color-lighter);
|
||||
z-index: 9;
|
||||
display: none;
|
||||
|
||||
|
||||
&.visible {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.mode-selector {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
|
||||
&.collapsed {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
@ -382,7 +365,7 @@ onUnmounted(() => {
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 60px;
|
||||
|
||||
|
||||
.mode-header-collapsed-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -390,12 +373,12 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.mode-menu {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
@ -421,6 +404,7 @@ onUnmounted(() => {
|
|||
.settings-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
.mode-name,
|
||||
.button-text {
|
||||
opacity: 0;
|
||||
|
@ -458,12 +442,12 @@ onUnmounted(() => {
|
|||
padding: 0;
|
||||
color: #f56c6c;
|
||||
flex-shrink: 0;
|
||||
|
||||
|
||||
&:hover {
|
||||
color: #f56c6c;
|
||||
background-color: rgba(245, 108, 108, 0.1);
|
||||
}
|
||||
|
||||
|
||||
:deep(.el-icon) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
@ -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>
|
||||
</style>
|
||||
|
|
Loading…
Reference in New Issue