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 [
|
export default [
|
||||||
// 获取聊天模式配置
|
// 获取聊天模式配置
|
||||||
{
|
{
|
||||||
url: '/api/chat/modes',
|
url: 'api/chat/modes',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
response: () => {
|
response: () => {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,53 +1,45 @@
|
||||||
import { getChatModes, sendMessage } from '@/api/chat'
|
import { getChatModes, sendMessage } from '@/api/chat'
|
||||||
import { useSpeakStore } from '@/store/speak'
|
import { useSpeakStore } from '@/store/speak'
|
||||||
|
import { processTagContent } from '@/utils'
|
||||||
import { speakText } from '@/utils/speak'
|
import { speakText } from '@/utils/speak'
|
||||||
import { defineStore } from 'pinia'
|
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 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', {
|
export const useChatStore = defineStore('chat', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
|
@ -55,11 +47,11 @@ export const useChatStore = defineStore('chat', {
|
||||||
currentMode: 'training',
|
currentMode: 'training',
|
||||||
conversationId: null,
|
conversationId: null,
|
||||||
messageCache: {},
|
messageCache: {},
|
||||||
chatModes: {},
|
|
||||||
chatModesLoading: false,
|
chatModesLoading: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
|
chatModes: () => chatModes,
|
||||||
currentToken: (state) => state.chatModes[state.currentMode]?.token,
|
currentToken: (state) => state.chatModes[state.currentMode]?.token,
|
||||||
currentConversation: (state) => {
|
currentConversation: (state) => {
|
||||||
return state.conversationId ? state.messages[state.conversationId] : null
|
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">
|
||||||
<div class="message-wrapper user">
|
<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-content">
|
||||||
<div class="message-text-wrapper">
|
<div class="message-text-wrapper">
|
||||||
<div v-if="message.createdAt" class="message-timestamp">
|
<div v-if="message.createdAt" class="message-timestamp">
|
||||||
|
@ -53,7 +53,7 @@
|
||||||
<!-- 回答 -->
|
<!-- 回答 -->
|
||||||
<div class="message">
|
<div class="message">
|
||||||
<div class="message-wrapper assistant">
|
<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-content">
|
||||||
<div class="message-text-wrapper">
|
<div class="message-text-wrapper">
|
||||||
<div v-if="message.createdAt" class="message-timestamp">
|
<div v-if="message.createdAt" class="message-timestamp">
|
||||||
|
@ -155,13 +155,20 @@ const lastMessageWithDafen = computed(() => {
|
||||||
|
|
||||||
// 获取头像
|
// 获取头像
|
||||||
const customerAvatar = computed(() => {
|
const customerAvatar = computed(() => {
|
||||||
return chatStore.chatModes[chatStore.currentMode]?.customerAvatar
|
const avatar = chatStore.chatModes[chatStore.currentMode]?.customerAvatar
|
||||||
|
return avatar || '/logo.svg'
|
||||||
})
|
})
|
||||||
|
|
||||||
const userAvatar = computed(() => {
|
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选项
|
||||||
marked.setOptions({
|
marked.setOptions({
|
||||||
breaks: true,
|
breaks: true,
|
||||||
|
@ -293,13 +300,6 @@ const formatMarkdown = (text, tagType) => {
|
||||||
return marked.parse(text)
|
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 = () => {
|
const toggleSummary = () => {
|
||||||
|
|
|
@ -23,19 +23,17 @@
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
|
||||||
<!-- 加载状态 -->
|
<!-- 加载状态 -->
|
||||||
<div v-if="chatStore.chatModesLoading" class="loading-container">
|
<!-- <div v-if="chatStore.chatModesLoading" class="loading-container">
|
||||||
<el-skeleton :rows="3" animated />
|
<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-menu-item v-for="mode in chatModes" :key="mode.id" :index="mode.id" class="mode-item">
|
<el-icon>
|
||||||
<el-icon>
|
<component :is="mode.icon" />
|
||||||
<component :is="mode.icon" />
|
</el-icon>
|
||||||
</el-icon>
|
<span class="mode-name">{{ mode.name }}</span>
|
||||||
<span class="mode-name">{{ mode.name }}</span>
|
</el-menu-item>
|
||||||
</el-menu-item>
|
|
||||||
</template>
|
|
||||||
</el-menu>
|
</el-menu>
|
||||||
<ChatHistoryList />
|
<ChatHistoryList />
|
||||||
<!-- Settings Button Popover -->
|
<!-- Settings Button Popover -->
|
||||||
|
@ -64,7 +62,7 @@ import { useChatStore } from '@/store/chat'
|
||||||
import { useSettingsStore } from '@/store/settings'
|
import { useSettingsStore } from '@/store/settings'
|
||||||
import ChatHistoryList from '@/views/chat/components/ChatHistoryList.vue'
|
import ChatHistoryList from '@/views/chat/components/ChatHistoryList.vue'
|
||||||
import { ChatDotRound, ChatLineRound, ChatRound, Plus, Service, Setting } from '@element-plus/icons-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'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
const chatStore = useChatStore()
|
const chatStore = useChatStore()
|
||||||
|
@ -80,14 +78,15 @@ const loadChatModes = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatModes = computed(() => {
|
const chatModes = computed(() => {
|
||||||
console.log(chatStore, 'chatModes')
|
const modes = Object.keys(chatStore.chatModes).map(key => ({
|
||||||
return Object.keys(chatStore.chatModes).map(key => ({
|
|
||||||
id: key,
|
id: key,
|
||||||
name: chatStore.chatModes[key].name,
|
name: chatStore.chatModes[key].name,
|
||||||
icon: key === 'general' ? ChatRound :
|
icon: key === 'general' ? ChatRound :
|
||||||
key === 'assistant' ? Service :
|
key === 'assistant' ? Service :
|
||||||
key === 'chat' ? ChatDotRound : ChatLineRound
|
key === 'chat' ? ChatDotRound : ChatLineRound
|
||||||
}))
|
}))
|
||||||
|
console.log('%c chatModes:', 'color: #2196F3; font-weight: bold', modes)
|
||||||
|
return modes
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -115,7 +114,7 @@ onMounted(() => {
|
||||||
checkMobile()
|
checkMobile()
|
||||||
window.addEventListener('resize', checkMobile)
|
window.addEventListener('resize', checkMobile)
|
||||||
// 加载聊天模式
|
// 加载聊天模式
|
||||||
loadChatModes()
|
// loadChatModes()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|
|
@ -29,7 +29,7 @@ export default defineConfig(({ command, mode }) => {
|
||||||
viteMockServe({
|
viteMockServe({
|
||||||
mockPath: 'src/mocks',
|
mockPath: 'src/mocks',
|
||||||
localEnabled: true,
|
localEnabled: true,
|
||||||
prodEnabled: false,
|
prodEnabled: true,
|
||||||
supportTs: true,
|
supportTs: true,
|
||||||
watchFiles: true,
|
watchFiles: true,
|
||||||
logger: true,
|
logger: true,
|
||||||
|
|
Loading…
Reference in New Issue