feat(chat): 重构聊天模块并添加新功能
- 重新定义聊天模式,增加 AI 寿险代理人、报价中异议和报价后异议等场景 - 添加自定义头像和背景信息,提升用户体验 - 优化消息处理逻辑,支持标签内容处理 - 调整界面布局和样式,增加头像加载错误处理 - 移除不必要的代码,提高代码可维护性
This commit is contained in:
parent
5a8c819c35
commit
5e4f45333b
Binary file not shown.
After Width: | Height: | Size: 2.1 MiB |
Binary file not shown.
After Width: | Height: | Size: 1.8 MiB |
|
@ -36,7 +36,7 @@ const chatModes = {
|
|||
export default [
|
||||
// 获取聊天模式配置
|
||||
{
|
||||
url: '/api/chat/modes',
|
||||
url: 'api/chat/modes',
|
||||
method: 'POST',
|
||||
response: () => {
|
||||
return {
|
||||
|
|
|
@ -1,53 +1,45 @@
|
|||
import { getChatModes, sendMessage } from '@/api/chat'
|
||||
import { useSpeakStore } from '@/store/speak'
|
||||
import { processTagContent } from '@/utils'
|
||||
import { speakText } from '@/utils/speak'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
const chatModes = {
|
||||
training: {
|
||||
name: "寿险代理人AI陪练",
|
||||
customerAvatar: "/images/avatars/custom.png",
|
||||
userAvatar: "/images/avatars/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: "/images/avatars/user.png",
|
||||
userAvatar: "/images/avatars/custom.png",
|
||||
token: "app-eYoE51WXCaEKXvzb9ZCj6lBn",
|
||||
background: "# AI寿险代理人\n### **角色设定** \n**身份**:AI寿险代理人 \n**特点**:专业、耐心、细致、富有同理心 \n**能力**:\n- 精通各类寿险产品知识\n- 擅长需求分析和方案定制\n- 具备优秀的沟通和谈判技巧\n- 能够处理各类客户异议\n- 熟悉保险行业最新动态",
|
||||
chatBackground: "一位AI的保险代理人,通过聊天来确认用户的需求,收集用户的基本信息"
|
||||
},
|
||||
quote_objection: {
|
||||
name: "报价中异议",
|
||||
customerAvatar: "/images/avatars/custom.png",
|
||||
userAvatar: "/images/avatars/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: "/images/avatars/custom.png",
|
||||
userAvatar: "/images/avatars/user.png",
|
||||
token: "app-Yiccl0JoXs2QF2lkHxO6f822",
|
||||
background: "# 客户信息\n### **基础背景与性格设定** \n**姓名**: 张灵女士\n**年龄**: 32岁\n**职业**: 公司行政主管\n**车牌号**: 粤B56789\n**车辆信息**:拥有一辆行驶4年多的本田思域,1.5T,CVT燃动版,购买价格约15万,用于日常通勤及周末短途出行。\n**投保信息**:在其他保司购买交强险及商业险(三者险、车损险、不计免赔险、车上人员责任险),还有2个月到期。无事故记录,仅有两次违章停车记录。张女士注重性价比和服务质量,希望保险的保障全面、价格合理、服务贴心。",
|
||||
chatBackground: "现在你将扮演坐席专员,与系统扮演的客户针对报价后的各类异议开展对练,着重训练处理\"报价后异议\"的能力。按照\"保全保足\"原则给出险种推荐方案。"
|
||||
}
|
||||
}
|
||||
const tagList = ['kehu', 'pingfen', 'zongjie', 'huaxiang', 'dafen']
|
||||
|
||||
// 处理标签内容的辅助函数
|
||||
const processTagContent = (answerCache, tagName, cachedMessage) => {
|
||||
// 检查是否包含完整的开始标签
|
||||
const startTag = `<${tagName}>`
|
||||
const endTag = `</${tagName}>`
|
||||
|
||||
// 如果正在处理该标签
|
||||
if (cachedMessage.respondingType === tagName) {
|
||||
// 检查是否包含结束标签
|
||||
if (answerCache.includes(endTag)) {
|
||||
// 提取结束标签前的内容
|
||||
const content = answerCache.split(endTag)[0]
|
||||
cachedMessage[tagName] += content
|
||||
cachedMessage.respondingType = ''
|
||||
cachedMessage[`${tagName}Finished`] = true
|
||||
return answerCache.split(endTag)[1] || ''
|
||||
} else {
|
||||
// 继续累积内容
|
||||
cachedMessage[tagName] += answerCache
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否包含开始标签
|
||||
if (answerCache.includes(startTag)) {
|
||||
cachedMessage.respondingType = tagName
|
||||
const tagContent = answerCache.split(startTag)[1] || ''
|
||||
|
||||
// 检查开始标签后是否立即包含结束标签
|
||||
if (tagContent.includes(endTag)) {
|
||||
const content = tagContent.split(endTag)[0]
|
||||
cachedMessage[tagName] = content
|
||||
cachedMessage.respondingType = ''
|
||||
return answerCache.split(endTag)[1] || ''
|
||||
} else {
|
||||
// 只有开始标签,累积内容
|
||||
cachedMessage[tagName] = tagContent
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
return answerCache
|
||||
}
|
||||
|
||||
export const useChatStore = defineStore('chat', {
|
||||
state: () => ({
|
||||
|
@ -55,11 +47,11 @@ export const useChatStore = defineStore('chat', {
|
|||
currentMode: 'training',
|
||||
conversationId: null,
|
||||
messageCache: {},
|
||||
chatModes: {},
|
||||
chatModesLoading: false,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
chatModes: () => chatModes,
|
||||
currentToken: (state) => state.chatModes[state.currentMode]?.token,
|
||||
currentConversation: (state) => {
|
||||
return state.conversationId ? state.messages[state.conversationId] : null
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
// 处理标签内容的辅助函数
|
||||
export const processTagContent = (answerCache, tagName, cachedMessage) => {
|
||||
// 检查是否包含完整的开始标签
|
||||
const startTag = `<${tagName}>`
|
||||
const endTag = `</${tagName}>`
|
||||
|
||||
// 如果正在处理该标签
|
||||
if (cachedMessage.respondingType === tagName) {
|
||||
// 检查是否包含结束标签
|
||||
if (answerCache.includes(endTag)) {
|
||||
// 提取结束标签前的内容
|
||||
const content = answerCache.split(endTag)[0]
|
||||
cachedMessage[tagName] += content
|
||||
cachedMessage.respondingType = ''
|
||||
cachedMessage[`${tagName}Finished`] = true
|
||||
return answerCache.split(endTag)[1] || ''
|
||||
} else {
|
||||
// 继续累积内容
|
||||
cachedMessage[tagName] += answerCache
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否包含开始标签
|
||||
if (answerCache.includes(startTag)) {
|
||||
cachedMessage.respondingType = tagName
|
||||
const tagContent = answerCache.split(startTag)[1] || ''
|
||||
|
||||
// 检查开始标签后是否立即包含结束标签
|
||||
if (tagContent.includes(endTag)) {
|
||||
const content = tagContent.split(endTag)[0]
|
||||
cachedMessage[tagName] = content
|
||||
cachedMessage.respondingType = ''
|
||||
return answerCache.split(endTag)[1] || ''
|
||||
} else {
|
||||
// 只有开始标签,累积内容
|
||||
cachedMessage[tagName] = tagContent
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
return answerCache
|
||||
}
|
|
@ -19,7 +19,7 @@
|
|||
<!-- 用户消息 -->
|
||||
<div class="message">
|
||||
<div class="message-wrapper user">
|
||||
<el-avatar :src="userAvatar" :size="40" class="avatar" />
|
||||
<el-avatar :src="userAvatar" :size="40" class="avatar" @error="handleAvatarError" />
|
||||
<div class="message-content">
|
||||
<div class="message-text-wrapper">
|
||||
<div v-if="message.createdAt" class="message-timestamp">
|
||||
|
@ -53,7 +53,7 @@
|
|||
<!-- 回答 -->
|
||||
<div class="message">
|
||||
<div class="message-wrapper assistant">
|
||||
<el-avatar :src="customerAvatar" :size="40" class="avatar" />
|
||||
<el-avatar :src="customerAvatar" :size="40" class="avatar" @error="handleAvatarError" />
|
||||
<div class="message-content">
|
||||
<div class="message-text-wrapper">
|
||||
<div v-if="message.createdAt" class="message-timestamp">
|
||||
|
@ -155,13 +155,20 @@ const lastMessageWithDafen = computed(() => {
|
|||
|
||||
// 获取头像
|
||||
const customerAvatar = computed(() => {
|
||||
return chatStore.chatModes[chatStore.currentMode]?.customerAvatar
|
||||
const avatar = chatStore.chatModes[chatStore.currentMode]?.customerAvatar
|
||||
return avatar || '/logo.svg'
|
||||
})
|
||||
|
||||
const userAvatar = computed(() => {
|
||||
return chatStore.chatModes[chatStore.currentMode]?.userAvatar
|
||||
const avatar = chatStore.chatModes[chatStore.currentMode]?.userAvatar
|
||||
return avatar || '/logo.svg'
|
||||
})
|
||||
|
||||
// 处理头像加载错误
|
||||
const handleAvatarError = (event) => {
|
||||
event.target.src = '/logo.svg'
|
||||
}
|
||||
|
||||
// 配置marked选项
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
|
@ -293,13 +300,6 @@ const formatMarkdown = (text, tagType) => {
|
|||
return marked.parse(text)
|
||||
}
|
||||
|
||||
// 调整文本区域高度
|
||||
const adjustTextareaHeight = () => {
|
||||
const textarea = messageInputRef.value
|
||||
textarea.style.height = 'auto'
|
||||
const newHeight = Math.min(textarea.scrollHeight, maxRows * 24)
|
||||
textarea.style.height = newHeight + 'px'
|
||||
}
|
||||
|
||||
// 切换总结面板
|
||||
const toggleSummary = () => {
|
||||
|
|
|
@ -23,19 +23,17 @@
|
|||
</el-menu-item>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="chatStore.chatModesLoading" class="loading-container">
|
||||
<!-- <div v-if="chatStore.chatModesLoading" class="loading-container">
|
||||
<el-skeleton :rows="3" animated />
|
||||
</div>
|
||||
</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-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>
|
||||
</el-menu>
|
||||
<ChatHistoryList />
|
||||
<!-- Settings Button Popover -->
|
||||
|
@ -64,7 +62,7 @@ import { useChatStore } from '@/store/chat'
|
|||
import { useSettingsStore } from '@/store/settings'
|
||||
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 { ElIcon, ElMenu, ElMenuItem, ElPopover } from 'element-plus'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const chatStore = useChatStore()
|
||||
|
@ -80,14 +78,15 @@ const loadChatModes = async () => {
|
|||
}
|
||||
|
||||
const chatModes = computed(() => {
|
||||
console.log(chatStore, 'chatModes')
|
||||
return Object.keys(chatStore.chatModes).map(key => ({
|
||||
const modes = Object.keys(chatStore.chatModes).map(key => ({
|
||||
id: key,
|
||||
name: chatStore.chatModes[key].name,
|
||||
icon: key === 'general' ? ChatRound :
|
||||
key === 'assistant' ? Service :
|
||||
key === 'chat' ? ChatDotRound : ChatLineRound
|
||||
}))
|
||||
console.log('%c chatModes:', 'color: #2196F3; font-weight: bold', modes)
|
||||
return modes
|
||||
})
|
||||
|
||||
|
||||
|
@ -115,7 +114,7 @@ onMounted(() => {
|
|||
checkMobile()
|
||||
window.addEventListener('resize', checkMobile)
|
||||
// 加载聊天模式
|
||||
loadChatModes()
|
||||
// loadChatModes()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
|
|
@ -29,7 +29,7 @@ export default defineConfig(({ command, mode }) => {
|
|||
viteMockServe({
|
||||
mockPath: 'src/mocks',
|
||||
localEnabled: true,
|
||||
prodEnabled: false,
|
||||
prodEnabled: true,
|
||||
supportTs: true,
|
||||
watchFiles: true,
|
||||
logger: true,
|
||||
|
|
Loading…
Reference in New Issue