agent_9art/frontend/sidebar/src/components/ConversationAnalysis.tsx
jiao d327826c78 chore: 完成项目整体打包与配置更新
1. 更新所有服务Dockerfile,切换到eclipse-temurin:17-jre-alpine镜像并调整服务端口
2. 配置前端项目基础路径与nginx代理
3. 新增数据库初始化编码设置与测试依赖
4. 添加部分服务的Servlet编码配置与JWT/LLM密钥配置
5. 完善话术实体类校验注解与Mapper接口
6. 新增密码生成工具类与部分测试文件配置
2026-05-21 11:40:22 +08:00

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>
)
}