ai-robot-channel/target/classes/static/index.html

639 lines
22 KiB
HTML
Raw Normal View History

2026-02-23 01:45:23 +00:00
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>人工客服工作台</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f5;
height: 100vh;
display: flex;
}
.sidebar {
width: 300px;
background: #fff;
border-right: 1px solid #e0e0e0;
display: flex;
flex-direction: column;
}
.sidebar-header {
padding: 15px;
background: #1890ff;
color: white;
font-size: 16px;
font-weight: bold;
}
.session-tabs {
display: flex;
border-bottom: 1px solid #e0e0e0;
}
.session-tab {
flex: 1;
padding: 10px;
text-align: center;
cursor: pointer;
border-bottom: 2px solid transparent;
}
.session-tab.active {
border-bottom-color: #1890ff;
color: #1890ff;
}
.session-list {
flex: 1;
overflow-y: auto;
}
.session-item {
padding: 12px 15px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
}
.session-item:hover {
background: #f5f5f5;
}
.session-item.active {
background: #e6f7ff;
}
.session-item .customer-id {
font-weight: bold;
margin-bottom: 5px;
}
.session-item .last-msg {
font-size: 12px;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.session-item .time {
font-size: 11px;
color: #999;
margin-top: 3px;
}
.status-badge {
display: inline-block;
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
margin-left: 5px;
}
.status-pending {
background: #fff7e6;
color: #fa8c16;
}
.status-manual {
background: #e6f7ff;
color: #1890ff;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
}
.chat-header {
padding: 15px;
background: #fff;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-messages {
flex: 1;
padding: 20px;
overflow-y: auto;
background: #f9f9f9;
}
.message {
margin-bottom: 15px;
display: flex;
}
.message.customer {
justify-content: flex-start;
}
.message.ai, .message.manual {
justify-content: flex-end;
}
.message-content {
max-width: 60%;
padding: 10px 15px;
border-radius: 8px;
position: relative;
}
.message.customer .message-content {
background: #fff;
border: 1px solid #e0e0e0;
}
.message.ai .message-content {
background: #e6f7ff;
border: 1px solid #91d5ff;
}
.message.manual .message-content {
background: #f6ffed;
border: 1px solid #b7eb8f;
}
.message-sender {
font-size: 11px;
color: #999;
margin-bottom: 3px;
}
.message-time {
font-size: 10px;
color: #bbb;
margin-top: 3px;
}
.chat-input {
padding: 15px;
background: #fff;
border-top: 1px solid #e0e0e0;
display: flex;
gap: 10px;
}
.chat-input textarea {
flex: 1;
padding: 10px;
border: 1px solid #d9d9d9;
border-radius: 4px;
resize: none;
height: 60px;
}
.chat-input button {
padding: 10px 20px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.chat-input button:hover {
background: #40a9ff;
}
.chat-input button:disabled {
background: #d9d9d9;
cursor: not-allowed;
}
.empty-state {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: #999;
}
.connection-status {
padding: 5px 10px;
font-size: 12px;
border-radius: 4px;
}
.connected {
background: #f6ffed;
color: #52c41a;
}
.disconnected {
background: #fff2f0;
color: #ff4d4f;
}
.actions {
display: flex;
gap: 10px;
}
.actions button {
padding: 5px 10px;
border: 1px solid #d9d9d9;
background: #fff;
border-radius: 4px;
cursor: pointer;
}
.actions button:hover {
border-color: #1890ff;
color: #1890ff;
}
.test-panel {
position: fixed;
right: 20px;
bottom: 20px;
background: #fff;
border: 1px solid #d9d9d9;
border-radius: 8px;
padding: 15px;
width: 300px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.test-panel h4 {
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid #f0f0f0;
}
.test-panel input, .test-panel textarea {
width: 100%;
padding: 8px;
margin-bottom: 10px;
border: 1px solid #d9d9d9;
border-radius: 4px;
}
.test-panel button {
width: 100%;
padding: 8px;
background: #52c41a;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.test-panel button:hover {
background: #73d13d;
}
</style>
</head>
<body>
<div class="sidebar">
<div class="sidebar-header">
客服工作台 <span id="csId">CS_001</span>
</div>
<div class="session-tabs">
<div class="session-tab active" data-status="PENDING" onclick="switchTab('PENDING')">
待接入 (<span id="pendingCount">0</span>)
</div>
<div class="session-tab" data-status="MANUAL" onclick="switchTab('MANUAL')">
进行中 (<span id="manualCount">0</span>)
</div>
</div>
<div class="session-list" id="sessionList">
</div>
</div>
<div class="main-content">
<div id="chatArea" style="display: none; height: 100%; flex-direction: column;">
<div class="chat-header">
<div>
<strong id="currentCustomer">-</strong>
<span class="status-badge" id="currentStatus">-</span>
</div>
<div class="actions">
<button onclick="acceptSession()" id="acceptBtn">接入会话</button>
<button onclick="closeSession()" id="closeBtn">结束会话</button>
</div>
</div>
<div class="chat-messages" id="chatMessages">
</div>
<div class="chat-input">
<textarea id="messageInput" placeholder="输入消息..."></textarea>
<button onclick="sendMessage()" id="sendBtn" disabled>发送</button>
</div>
</div>
<div id="emptyState" class="empty-state">
<div style="text-align: center;">
<p>请从左侧选择一个会话</p>
<p style="margin-top: 10px; font-size: 12px;">WebSocket: <span id="wsStatus" class="connection-status disconnected">未连接</span></p>
</div>
</div>
</div>
<div class="test-panel">
<h4>🧪 模拟客户消息</h4>
<input type="text" id="testCustomerId" placeholder="客户ID" value="test_customer_001">
<input type="text" id="testKfId" placeholder="客服账号ID" value="test_kf_001">
<textarea id="testContent" placeholder="消息内容"></textarea>
<button onclick="sendTestMessage()">发送测试消息</button>
<button onclick="triggerTransfer()" style="margin-top: 5px; background: #fa8c16;">触发转人工</button>
</div>
<script>
let ws = null;
let currentSessionId = null;
let currentStatus = null;
let csId = 'CS_001';
const baseUrl = window.location.origin;
function connectWebSocket() {
const wsUrl = baseUrl.replace('http', 'ws') + '/ws/cs/' + csId;
ws = new WebSocket(wsUrl);
ws.onopen = function() {
document.getElementById('wsStatus').className = 'connection-status connected';
document.getElementById('wsStatus').textContent = '已连接';
console.log('WebSocket已连接');
};
ws.onclose = function() {
document.getElementById('wsStatus').className = 'connection-status disconnected';
document.getElementById('wsStatus').textContent = '已断开';
console.log('WebSocket已断开');
setTimeout(connectWebSocket, 3000);
};
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
console.log('收到消息:', data);
handleWebSocketMessage(data);
};
ws.onerror = function(error) {
console.error('WebSocket错误:', error);
};
}
function handleWebSocketMessage(data) {
switch(data.type) {
case 'new_pending_session':
alert('有新的待接入会话!');
loadSessions();
break;
case 'new_message':
case 'customer_message':
if (currentSessionId === data.sessionId) {
addMessage('customer', data.content, data.timestamp);
}
loadSessions();
break;
case 'session_accepted':
if (currentSessionId === data.sessionId) {
currentStatus = 'MANUAL';
updateChatHeader();
document.getElementById('sendBtn').disabled = false;
document.getElementById('acceptBtn').disabled = true;
}
break;
case 'session_closed':
if (currentSessionId === data.sessionId) {
alert('会话已结束');
currentSessionId = null;
showEmptyState();
}
loadSessions();
break;
}
}
function switchTab(status) {
document.querySelectorAll('.session-tab').forEach(tab => {
tab.classList.remove('active');
if (tab.dataset.status === status) {
tab.classList.add('active');
}
});
loadSessions(status);
}
async function loadSessions(status = 'PENDING') {
try {
const response = await fetch(baseUrl + '/api/sessions?status=' + status);
const result = await response.json();
if (result.code === 200) {
renderSessionList(result.data, status);
if (status === 'PENDING') {
document.getElementById('pendingCount').textContent = result.data.length;
} else {
document.getElementById('manualCount').textContent = result.data.length;
}
}
} catch (error) {
console.error('加载会话列表失败:', error);
}
}
function renderSessionList(sessions, status) {
const list = document.getElementById('sessionList');
list.innerHTML = '';
sessions.forEach(session => {
const item = document.createElement('div');
item.className = 'session-item' + (currentSessionId === session.sessionId ? ' active' : '');
item.onclick = () => selectSession(session);
const time = session.lastMessageTime ? new Date(session.lastMessageTime).toLocaleString() : '-';
item.innerHTML = `
<div class="customer-id">
${session.customerId}
<span class="status-badge status-${session.status.toLowerCase()}">${session.status}</span>
</div>
<div class="last-msg">${session.lastMessage || '暂无消息'}</div>
<div class="time">${time}</div>
`;
list.appendChild(item);
});
if (sessions.length === 0) {
list.innerHTML = '<div style="padding: 20px; text-align: center; color: #999;">暂无会话</div>';
}
}
async function selectSession(session) {
currentSessionId = session.sessionId;
currentStatus = session.status;
document.querySelectorAll('.session-item').forEach(item => {
item.classList.remove('active');
});
event.currentTarget.classList.add('active');
document.getElementById('emptyState').style.display = 'none';
document.getElementById('chatArea').style.display = 'flex';
updateChatHeader();
await loadHistory();
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'bind_session',
sessionId: currentSessionId
}));
}
}
function updateChatHeader() {
document.getElementById('currentCustomer').textContent = currentSessionId;
const statusBadge = document.getElementById('currentStatus');
statusBadge.textContent = currentStatus;
statusBadge.className = 'status-badge status-' + currentStatus.toLowerCase();
document.getElementById('acceptBtn').disabled = currentStatus !== 'PENDING';
document.getElementById('sendBtn').disabled = currentStatus !== 'MANUAL';
document.getElementById('closeBtn').disabled = currentStatus !== 'MANUAL';
}
async function loadHistory() {
try {
const response = await fetch(baseUrl + '/api/sessions/' + currentSessionId + '/history');
const result = await response.json();
if (result.code === 200) {
const container = document.getElementById('chatMessages');
container.innerHTML = '';
result.data.forEach(msg => {
addMessage(msg.senderType, msg.content, msg.createdAt);
});
container.scrollTop = container.scrollHeight;
}
} catch (error) {
console.error('加载历史消息失败:', error);
}
}
function addMessage(senderType, content, timestamp) {
const container = document.getElementById('chatMessages');
const msg = document.createElement('div');
msg.className = 'message ' + senderType;
const senderName = senderType === 'customer' ? '客户' :
senderType === 'ai' ? 'AI客服' : '人工客服';
const time = timestamp ? new Date(timestamp).toLocaleString() : new Date().toLocaleString();
msg.innerHTML = `
<div class="message-content">
<div class="message-sender">${senderName}</div>
<div>${content}</div>
<div class="message-time">${time}</div>
</div>
`;
container.appendChild(msg);
container.scrollTop = container.scrollHeight;
}
async function acceptSession() {
if (!currentSessionId) return;
try {
const response = await fetch(baseUrl + '/api/sessions/' + currentSessionId + '/accept', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ csId: csId })
});
const result = await response.json();
if (result.code === 200) {
currentStatus = 'MANUAL';
updateChatHeader();
loadSessions('PENDING');
loadSessions('MANUAL');
} else {
alert('接入失败: ' + result.message);
}
} catch (error) {
console.error('接入会话失败:', error);
}
}
async function sendMessage() {
if (!currentSessionId || currentStatus !== 'MANUAL') return;
const content = document.getElementById('messageInput').value.trim();
if (!content) return;
try {
const response = await fetch(baseUrl + '/api/sessions/' + currentSessionId + '/message', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: content, msgType: 'text' })
});
const result = await response.json();
if (result.code === 200) {
addMessage('manual', content);
document.getElementById('messageInput').value = '';
} else {
alert('发送失败: ' + result.message);
}
} catch (error) {
console.error('发送消息失败:', error);
}
}
async function closeSession() {
if (!currentSessionId) return;
try {
const response = await fetch(baseUrl + '/api/sessions/' + currentSessionId + '/close', {
method: 'POST'
});
const result = await response.json();
if (result.code === 200) {
currentSessionId = null;
showEmptyState();
loadSessions('PENDING');
loadSessions('MANUAL');
}
} catch (error) {
console.error('结束会话失败:', error);
}
}
function showEmptyState() {
document.getElementById('emptyState').style.display = 'flex';
document.getElementById('chatArea').style.display = 'none';
}
async function sendTestMessage() {
const customerId = document.getElementById('testCustomerId').value;
const kfId = document.getElementById('testKfId').value;
const content = document.getElementById('testContent').value;
if (!content) {
alert('请输入消息内容');
return;
}
try {
const response = await fetch(baseUrl + '/test/send-message?customerId=' + customerId + '&kfId=' + kfId + '&content=' + encodeURIComponent(content), {
method: 'POST'
});
const result = await response.json();
if (result.code === 200) {
alert('消息已发送!');
document.getElementById('testContent').value = '';
setTimeout(() => loadSessions('PENDING'), 500);
setTimeout(() => loadSessions('MANUAL'), 500);
}
} catch (error) {
console.error('发送测试消息失败:', error);
}
}
async function triggerTransfer() {
const customerId = document.getElementById('testCustomerId').value;
const kfId = document.getElementById('testKfId').value;
try {
const response = await fetch(baseUrl + '/test/trigger-transfer?customerId=' + customerId + '&kfId=' + kfId, {
method: 'POST'
});
const result = await response.json();
if (result.code === 200) {
alert('已触发转人工!');
setTimeout(() => loadSessions('PENDING'), 500);
}
} catch (error) {
console.error('触发转人工失败:', error);
}
}
document.getElementById('messageInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
connectWebSocket();
loadSessions('PENDING');
loadSessions('MANUAL');
</script>
</body>
</html>