1. 更新所有服务Dockerfile,切换到eclipse-temurin:17-jre-alpine镜像并调整服务端口 2. 配置前端项目基础路径与nginx代理 3. 新增数据库初始化编码设置与测试依赖 4. 添加部分服务的Servlet编码配置与JWT/LLM密钥配置 5. 完善话术实体类校验注解与Mapper接口 6. 新增密码生成工具类与部分测试文件配置
244 lines
8.4 KiB
TypeScript
244 lines
8.4 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { SpinLoading, Toast, Tag } from 'antd-mobile'
|
|
|
|
interface Conversation {
|
|
sessionId: string
|
|
customerId: string
|
|
customerName: string
|
|
staffName: string
|
|
currentStage: string
|
|
stageConfidence: number
|
|
roundCount: number
|
|
intentCount: number
|
|
startTime: string
|
|
endTime: string
|
|
contextSummary: string
|
|
keyEvents: string
|
|
}
|
|
|
|
interface ConversationTurn {
|
|
turnNumber: number
|
|
studentContent: string
|
|
seatContent: string
|
|
turnType: string
|
|
primaryIntent: string
|
|
intentConfidence: number
|
|
studentEmotion: string
|
|
createdAt: string
|
|
}
|
|
|
|
interface Props {
|
|
userInfo?: any
|
|
}
|
|
|
|
export default function ConversationAnalysis({ userInfo }: Props) {
|
|
const [conversations, setConversations] = useState<Conversation[]>([])
|
|
const [selectedSession, setSelectedSession] = useState<string>('')
|
|
const [turns, setTurns] = useState<ConversationTurn[]>([])
|
|
const [loading, setLoading] = useState(false)
|
|
const [loadingTurns, setLoadingTurns] = useState(false)
|
|
|
|
const customerId = localStorage.getItem('current_customer_id') || 'wx_001'
|
|
const corpId = userInfo?.corpId || 'wwd483c2fba24ae30a'
|
|
|
|
useEffect(() => {
|
|
loadConversations()
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
if (selectedSession) {
|
|
loadTurns(selectedSession)
|
|
}
|
|
}, [selectedSession])
|
|
|
|
const loadConversations = async () => {
|
|
setLoading(true)
|
|
try {
|
|
const res = await fetch(`/api/v1/conversations?customerId=${customerId}&corpId=${corpId}`)
|
|
const data = await res.json()
|
|
if (data.code === 0 && data.data) {
|
|
setConversations(data.data)
|
|
if (data.data.length > 0) {
|
|
setSelectedSession(data.data[0].sessionId)
|
|
}
|
|
}
|
|
} catch (e) {
|
|
Toast.show({ content: '加载会话失败', icon: 'fail' })
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const loadTurns = async (sessionId: string) => {
|
|
setLoadingTurns(true)
|
|
try {
|
|
const res = await fetch(`/api/v1/conversations/${sessionId}/turns`)
|
|
const data = await res.json()
|
|
if (data.code === 0 && data.data) {
|
|
setTurns(data.data)
|
|
}
|
|
} catch (e) {
|
|
Toast.show({ content: '加载对话失败', icon: 'fail' })
|
|
} finally {
|
|
setLoadingTurns(false)
|
|
}
|
|
}
|
|
|
|
const getStageLabel = (stage?: string) => {
|
|
const map: Record<string, string> = {
|
|
'STAGE-01': '开场白',
|
|
'STAGE-02': '需求挖掘',
|
|
'STAGE-03': '方案呈现',
|
|
'STAGE-04': '异议处理',
|
|
'STAGE-05': '促成签约',
|
|
'STAGE-06': '跟进维护',
|
|
'STAGE-07': '转介绍',
|
|
'STAGE-08': '沉默激活'
|
|
}
|
|
return map[stage || ''] || stage || '未知'
|
|
}
|
|
|
|
const getEmotionLabel = (emotion?: string) => {
|
|
const map: Record<string, { text: string; color: string }> = {
|
|
'POSITIVE': { text: '积极', color: '#52c41a' },
|
|
'NEUTRAL': { text: '中性', color: '#faad14' },
|
|
'NEGATIVE': { text: '消极', color: '#ff4d4f' }
|
|
}
|
|
return map[emotion || ''] || { text: emotion || '未知', color: '#999' }
|
|
}
|
|
|
|
const getTurnTypeLabel = (type?: string) => {
|
|
const map: Record<string, string> = {
|
|
'QUERY': '咨询',
|
|
'OBJECTION': '异议',
|
|
'INTEREST': '兴趣'
|
|
}
|
|
return map[type || ''] || type || '其他'
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div style={{ padding: 40, textAlign: 'center' }}>
|
|
<SpinLoading />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const currentConv = conversations.find(c => c.sessionId === selectedSession)
|
|
|
|
return (
|
|
<div style={{ padding: 12 }}>
|
|
{/* 会话选择器 */}
|
|
{conversations.length > 1 && (
|
|
<div style={{ marginBottom: 12, display: 'flex', gap: 8, overflowX: 'auto' }}>
|
|
{conversations.map(conv => (
|
|
<div
|
|
key={conv.sessionId}
|
|
onClick={() => setSelectedSession(conv.sessionId)}
|
|
style={{
|
|
padding: '6px 12px',
|
|
borderRadius: 16,
|
|
fontSize: 12,
|
|
whiteSpace: 'nowrap',
|
|
cursor: 'pointer',
|
|
background: selectedSession === conv.sessionId ? '#1677ff' : '#f0f0f0',
|
|
color: selectedSession === conv.sessionId ? '#fff' : '#666'
|
|
}}
|
|
>
|
|
{new Date(conv.startTime).toLocaleDateString()} ({conv.roundCount}轮)
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* 会话概览 */}
|
|
{currentConv && (
|
|
<div style={{ background: '#fff', borderRadius: 8, padding: 12, marginBottom: 12, boxShadow: '0 1px 4px rgba(0,0,0,0.06)' }}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
|
<span style={{ fontSize: 14, fontWeight: 600 }}>会话概览</span>
|
|
<Tag color="primary" fill="outline">{getStageLabel(currentConv.currentStage)}</Tag>
|
|
</div>
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 8, fontSize: 12, color: '#666' }}>
|
|
<div>轮次: <span style={{ color: '#333', fontWeight: 600 }}>{currentConv.roundCount}</span></div>
|
|
<div>意图: <span style={{ color: '#333', fontWeight: 600 }}>{currentConv.intentCount}</span></div>
|
|
<div>置信度: <span style={{ color: '#333', fontWeight: 600 }}>{Math.round((currentConv.stageConfidence || 0) * 100)}%</span></div>
|
|
</div>
|
|
{currentConv.contextSummary && (
|
|
<div style={{ marginTop: 8, padding: 8, background: '#f6ffed', borderRadius: 6, fontSize: 12, color: '#389e0d' }}>
|
|
摘要: {currentConv.contextSummary}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* 对话轮次 */}
|
|
<div style={{ fontSize: 14, fontWeight: 600, marginBottom: 10 }}>对话记录</div>
|
|
|
|
{loadingTurns ? (
|
|
<div style={{ padding: 40, textAlign: 'center' }}><SpinLoading /></div>
|
|
) : turns.length === 0 ? (
|
|
<div style={{ textAlign: 'center', padding: 40, color: '#999' }}>暂无对话记录</div>
|
|
) : (
|
|
<div>
|
|
{turns.map((turn) => {
|
|
const emotion = getEmotionLabel(turn.studentEmotion)
|
|
return (
|
|
<div key={turn.turnNumber} style={{ marginBottom: 16 }}>
|
|
{/* 轮次标识 */}
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
|
|
<span style={{ fontSize: 11, color: '#999' }}>第{turn.turnNumber}轮</span>
|
|
<Tag color={emotion.color} style={{ '--border-radius': '4px', fontSize: 11, padding: '0 4px' } as any}>
|
|
{emotion.text}
|
|
</Tag>
|
|
<Tag color="default" style={{ '--border-radius': '4px', fontSize: 11, padding: '0 4px' } as any}>
|
|
{getTurnTypeLabel(turn.turnType)}
|
|
</Tag>
|
|
{turn.primaryIntent && (
|
|
<span style={{ fontSize: 11, color: '#1677ff' }}>{turn.primaryIntent}</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* 学员消息 */}
|
|
{turn.studentContent && (
|
|
<div style={{ display: 'flex', marginBottom: 6 }}>
|
|
<div style={{
|
|
background: '#e6f4ff',
|
|
borderRadius: '12px 12px 12px 4px',
|
|
padding: '8px 12px',
|
|
maxWidth: '80%',
|
|
fontSize: 13,
|
|
color: '#333',
|
|
lineHeight: 1.5
|
|
}}>
|
|
<div style={{ fontSize: 11, color: '#1677ff', marginBottom: 2 }}>学员</div>
|
|
{turn.studentContent}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 顾问回复 */}
|
|
{turn.seatContent && (
|
|
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
|
<div style={{
|
|
background: '#f6ffed',
|
|
borderRadius: '12px 12px 4px 12px',
|
|
padding: '8px 12px',
|
|
maxWidth: '80%',
|
|
fontSize: 13,
|
|
color: '#333',
|
|
lineHeight: 1.5
|
|
}}>
|
|
<div style={{ fontSize: 11, color: '#52c41a', marginBottom: 2, textAlign: 'right' }}>顾问</div>
|
|
{turn.seatContent}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|