会话总结工作流接入、前后端处理
This commit is contained in:
@@ -11,7 +11,9 @@ import type {
|
||||
ChatMemberVO,
|
||||
ChatRoomMessageVO,
|
||||
CustomerServiceVO,
|
||||
VideoMeetingVO
|
||||
VideoMeetingVO,
|
||||
ChatRoomSummaryRequest,
|
||||
ChatRoomSummaryResponse
|
||||
} from '@/types/workcase'
|
||||
|
||||
/**
|
||||
@@ -286,5 +288,29 @@ export const workcaseChatAPI = {
|
||||
params: { commentLevel }
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
// ====================== 聊天室总结管理 ======================
|
||||
|
||||
/**
|
||||
* 获取聊天室最新总结
|
||||
* @param roomId 聊天室ID
|
||||
*/
|
||||
async getLatestSummary(roomId: string): Promise<ResultDomain<ChatRoomSummaryResponse>> {
|
||||
const response = await api.get<ChatRoomSummaryResponse>(`${this.baseUrl}/room/${roomId}/summary`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 生成聊天室对话总结
|
||||
* @param request 总结请求参数
|
||||
*/
|
||||
async summaryChatRoom(request: ChatRoomSummaryRequest): Promise<ResultDomain<ChatRoomSummaryResponse>> {
|
||||
const { roomId, ...body } = request
|
||||
const response = await api.post<ChatRoomSummaryResponse>(
|
||||
`${this.baseUrl}/room/${roomId}/summary`,
|
||||
Object.keys(body).length > 0 ? body : {}
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,4 +313,26 @@ export interface CreateMeetingParam {
|
||||
export interface MarkReadParam {
|
||||
roomId: string
|
||||
messageIds?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 聊天室总结请求参数
|
||||
*/
|
||||
export interface ChatRoomSummaryRequest {
|
||||
roomId: string
|
||||
includeSystemMessages?: boolean
|
||||
includeMeetingMessages?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 聊天室总结响应结果
|
||||
*/
|
||||
export interface ChatRoomSummaryResponse {
|
||||
question?: string
|
||||
needs?: string[]
|
||||
answer?: string
|
||||
workcloud?: string[]
|
||||
roomId?: string
|
||||
summaryTime?: string
|
||||
messageCount?: number
|
||||
}
|
||||
@@ -174,26 +174,166 @@
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
padding: 24px;
|
||||
min-height: 420px;
|
||||
}
|
||||
|
||||
.summary-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
gap: 16px;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
|
||||
.loading-spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid #e5e7eb;
|
||||
border-top-color: #4b87ff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.summary-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.summary-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.regenerate-btn {
|
||||
padding: 6px 16px;
|
||||
background: #fff;
|
||||
color: #4b87ff;
|
||||
border: 1px solid #4b87ff;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #4b87ff;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.summary-section {
|
||||
.summary-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin-bottom: 12px;
|
||||
padding-left: 12px;
|
||||
border-left: 3px solid #4b87ff;
|
||||
}
|
||||
|
||||
.summary-text {
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
line-height: 1.6;
|
||||
color: #1f2937;
|
||||
line-height: 1.8;
|
||||
padding: 16px;
|
||||
background: #f9fafb;
|
||||
border-radius: 8px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.summary-list {
|
||||
margin: 0;
|
||||
padding-left: 24px;
|
||||
|
||||
li {
|
||||
font-size: 14px;
|
||||
color: #1f2937;
|
||||
line-height: 2;
|
||||
margin-bottom: 8px;
|
||||
position: relative;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&::marker {
|
||||
color: #4b87ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.summary-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 12px 16px;
|
||||
background: #fef3c7;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
color: #92400e;
|
||||
margin-top: 8px;
|
||||
|
||||
span {
|
||||
&:not(:last-child)::after {
|
||||
content: '•';
|
||||
margin-left: 16px;
|
||||
color: #d97706;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.summary-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
gap: 16px;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.generate-btn {
|
||||
padding: 10px 24px;
|
||||
background: #4b87ff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #3b77ef;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(75, 135, 255, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,19 +81,55 @@
|
||||
|
||||
<!-- 对话纪要 -->
|
||||
<div v-else class="summary-container">
|
||||
<div class="summary-content">
|
||||
<div class="summary-section">
|
||||
<div class="summary-title">问题概述</div>
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loadingSummary" class="summary-loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<span>正在生成对话总结...</span>
|
||||
</div>
|
||||
|
||||
<!-- 总结内容 -->
|
||||
<div v-else-if="summaryData" class="summary-content">
|
||||
<div class="summary-header">
|
||||
<h3>对话总结</h3>
|
||||
<button class="regenerate-btn" @click="generateSummary">重新生成</button>
|
||||
</div>
|
||||
|
||||
<div v-if="summaryData.question" class="summary-section">
|
||||
<div class="summary-title">核心问题</div>
|
||||
<div class="summary-text">
|
||||
{{ summary.overview || '暂无概述' }}
|
||||
{{ summaryData.question }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-section">
|
||||
<div class="summary-title">客户诉求</div>
|
||||
|
||||
<div v-if="summaryData.needs && summaryData.needs.length > 0" class="summary-section">
|
||||
<div class="summary-title">核心诉求</div>
|
||||
<ul class="summary-list">
|
||||
<li v-for="(need, index) in summaryData.needs" :key="index">{{ need }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="summaryData.answer" class="summary-section">
|
||||
<div class="summary-title">解决方案</div>
|
||||
<div class="summary-text">
|
||||
{{ summary.demand || '暂无诉求' }}
|
||||
{{ summaryData.answer }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="summary-meta">
|
||||
<span v-if="summaryData.messageCount">
|
||||
基于 {{ summaryData.messageCount }} 条消息生成
|
||||
</span>
|
||||
<span v-if="summaryData.summaryTime">
|
||||
生成时间:{{ summaryData.summaryTime }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="summary-empty">
|
||||
<div class="empty-icon">📝</div>
|
||||
<div class="empty-text">暂无对话总结</div>
|
||||
<button class="generate-btn" @click="generateSummary">生成总结</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -101,13 +137,9 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, computed, nextTick } from 'vue'
|
||||
import type { ChatRoomVO, ChatRoomMessageVO } from '@/types/workcase/chatRoom'
|
||||
import type { ChatRoomVO, ChatRoomMessageVO, ChatRoomSummaryResponse } from '@/types/workcase/chatRoom'
|
||||
import { workcaseChatAPI } from '@/api/workcase/workcaseChat'
|
||||
|
||||
interface Summary {
|
||||
overview?: string
|
||||
demand?: string
|
||||
}
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
chatRoom?: ChatRoomVO
|
||||
@@ -123,10 +155,8 @@ const activeTab = ref<'record' | 'summary'>('record')
|
||||
const messages = ref<ChatRoomMessageVO[]>([])
|
||||
const loading = ref(false)
|
||||
const loadingMore = ref(false)
|
||||
const summary = ref<Summary>({
|
||||
overview: '',
|
||||
demand: ''
|
||||
})
|
||||
const summaryData = ref<ChatRoomSummaryResponse | null>(null)
|
||||
const loadingSummary = ref(false)
|
||||
|
||||
// 分页相关
|
||||
const currentPage = ref(1)
|
||||
@@ -241,34 +271,82 @@ const handleScroll = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 生成对话纪要(可以后续接入AI生成)
|
||||
const generateSummary = () => {
|
||||
if (messages.value.length === 0) return
|
||||
|
||||
// 简单提取第一条和最后一条消息作为概述
|
||||
const firstMsg = messages.value[0]
|
||||
const lastMsg = messages.value[messages.value.length - 1]
|
||||
|
||||
summary.value = {
|
||||
overview: `客户反馈:${firstMsg.content?.substring(0, 50) || ''}`,
|
||||
demand: lastMsg.content?.substring(0, 100) || ''
|
||||
// 查询最新的对话总结
|
||||
const loadLatestSummary = async () => {
|
||||
if (!targetRoomId.value) {
|
||||
return
|
||||
}
|
||||
|
||||
loadingSummary.value = true
|
||||
summaryData.value = null
|
||||
|
||||
try {
|
||||
const res = await workcaseChatAPI.getLatestSummary(targetRoomId.value)
|
||||
|
||||
if (res.success && res.data) {
|
||||
summaryData.value = res.data
|
||||
} else {
|
||||
// 查询失败,表示还没有总结
|
||||
summaryData.value = null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('查询总结失败:', error)
|
||||
summaryData.value = null
|
||||
} finally {
|
||||
loadingSummary.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 生成对话纪要(调用后端AI接口)
|
||||
const generateSummary = async () => {
|
||||
if (!targetRoomId.value) {
|
||||
ElMessage.warning('无效的聊天室ID')
|
||||
return
|
||||
}
|
||||
|
||||
loadingSummary.value = true
|
||||
summaryData.value = null
|
||||
|
||||
try {
|
||||
const res = await workcaseChatAPI.summaryChatRoom({
|
||||
roomId: targetRoomId.value,
|
||||
includeSystemMessages: false,
|
||||
includeMeetingMessages: false
|
||||
})
|
||||
|
||||
if (res.success && res.data) {
|
||||
summaryData.value = res.data
|
||||
ElMessage.success('总结生成成功')
|
||||
} else {
|
||||
ElMessage.error(res.message || '生成总结失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('生成总结失败:', error)
|
||||
ElMessage.error('生成总结失败')
|
||||
} finally {
|
||||
loadingSummary.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadMessages()
|
||||
generateSummary()
|
||||
})
|
||||
|
||||
// 监听 roomId 变化,重新加载数据
|
||||
watch(targetRoomId, async (newVal) => {
|
||||
if (newVal) {
|
||||
messages.value = []
|
||||
summary.value = { overview: '', demand: '' }
|
||||
summaryData.value = null
|
||||
currentPage.value = 1
|
||||
total.value = 0
|
||||
await loadMessages()
|
||||
generateSummary()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听切换到对话纪要标签,自动查询总结
|
||||
watch(activeTab, async (newVal) => {
|
||||
if (newVal === 'summary' && !summaryData.value && !loadingSummary.value) {
|
||||
await loadLatestSummary()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user