411 lines
15 KiB
JavaScript
411 lines
15 KiB
JavaScript
|
|
// 实时对话前端脚本
|
|||
|
|
|
|||
|
|
class ChatClient {
|
|||
|
|
constructor() {
|
|||
|
|
this.websocket = null;
|
|||
|
|
this.sessionId = null;
|
|||
|
|
this.isConnected = false;
|
|||
|
|
this.messageCount = 0;
|
|||
|
|
|
|||
|
|
this.init();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
init() {
|
|||
|
|
this.bindEvents();
|
|||
|
|
this.updateConnectionStatus(false);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bindEvents() {
|
|||
|
|
// 开始对话
|
|||
|
|
document.getElementById('start-chat').addEventListener('click', () => this.startChat());
|
|||
|
|
|
|||
|
|
// 结束对话
|
|||
|
|
document.getElementById('end-chat').addEventListener('click', () => this.endChat());
|
|||
|
|
|
|||
|
|
// 发送消息
|
|||
|
|
document.getElementById('send-button').addEventListener('click', () => this.sendMessage());
|
|||
|
|
|
|||
|
|
// 回车发送
|
|||
|
|
document.getElementById('message-input').addEventListener('keypress', (e) => {
|
|||
|
|
if (e.key === 'Enter') {
|
|||
|
|
this.sendMessage();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 创建工单
|
|||
|
|
document.getElementById('create-work-order').addEventListener('click', () => this.showWorkOrderModal());
|
|||
|
|
document.getElementById('create-work-order-btn').addEventListener('click', () => this.createWorkOrder());
|
|||
|
|
|
|||
|
|
// 快速操作按钮
|
|||
|
|
document.querySelectorAll('.quick-action-btn').forEach(btn => {
|
|||
|
|
btn.addEventListener('click', (e) => {
|
|||
|
|
const message = e.target.getAttribute('data-message');
|
|||
|
|
document.getElementById('message-input').value = message;
|
|||
|
|
this.sendMessage();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async startChat() {
|
|||
|
|
try {
|
|||
|
|
// 连接WebSocket
|
|||
|
|
await this.connectWebSocket();
|
|||
|
|
|
|||
|
|
// 创建会话
|
|||
|
|
const userId = document.getElementById('user-id').value || 'anonymous';
|
|||
|
|
const workOrderId = document.getElementById('work-order-id').value || null;
|
|||
|
|
|
|||
|
|
const response = await this.sendWebSocketMessage({
|
|||
|
|
type: 'create_session',
|
|||
|
|
user_id: userId,
|
|||
|
|
work_order_id: workOrderId ? parseInt(workOrderId) : null
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (response.type === 'session_created') {
|
|||
|
|
this.sessionId = response.session_id;
|
|||
|
|
this.updateSessionInfo();
|
|||
|
|
this.enableChat();
|
|||
|
|
this.addSystemMessage('对话已开始,请描述您的问题。');
|
|||
|
|
} else {
|
|||
|
|
this.showError('创建会话失败');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('启动对话失败:', error);
|
|||
|
|
this.showError('启动对话失败: ' + error.message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async endChat() {
|
|||
|
|
try {
|
|||
|
|
if (this.sessionId) {
|
|||
|
|
await this.sendWebSocketMessage({
|
|||
|
|
type: 'end_session',
|
|||
|
|
session_id: this.sessionId
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.sessionId = null;
|
|||
|
|
this.disableChat();
|
|||
|
|
this.addSystemMessage('对话已结束。');
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('结束对话失败:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async sendMessage() {
|
|||
|
|
const input = document.getElementById('message-input');
|
|||
|
|
const message = input.value.trim();
|
|||
|
|
|
|||
|
|
if (!message || !this.sessionId) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 清空输入框
|
|||
|
|
input.value = '';
|
|||
|
|
|
|||
|
|
// 添加用户消息
|
|||
|
|
this.addMessage('user', message);
|
|||
|
|
|
|||
|
|
// 显示打字指示器
|
|||
|
|
this.showTypingIndicator();
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const response = await this.sendWebSocketMessage({
|
|||
|
|
type: 'send_message',
|
|||
|
|
session_id: this.sessionId,
|
|||
|
|
message: message
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.hideTypingIndicator();
|
|||
|
|
|
|||
|
|
if (response.type === 'message_response' && response.result.success) {
|
|||
|
|
const result = response.result;
|
|||
|
|
|
|||
|
|
// 添加助手回复
|
|||
|
|
this.addMessage('assistant', result.content, {
|
|||
|
|
knowledge_used: result.knowledge_used,
|
|||
|
|
confidence_score: result.confidence_score,
|
|||
|
|
work_order_id: result.work_order_id
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 更新工单ID
|
|||
|
|
if (result.work_order_id) {
|
|||
|
|
document.getElementById('work-order-id').value = result.work_order_id;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} else {
|
|||
|
|
this.addMessage('assistant', '抱歉,我暂时无法处理您的问题。请稍后再试。');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
this.hideTypingIndicator();
|
|||
|
|
console.error('发送消息失败:', error);
|
|||
|
|
this.addMessage('assistant', '发送消息失败,请检查网络连接。');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async createWorkOrder() {
|
|||
|
|
const title = document.getElementById('wo-title').value;
|
|||
|
|
const description = document.getElementById('wo-description').value;
|
|||
|
|
const category = document.getElementById('wo-category').value;
|
|||
|
|
const priority = document.getElementById('wo-priority').value;
|
|||
|
|
|
|||
|
|
if (!title || !description) {
|
|||
|
|
this.showError('请填写工单标题和描述');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const response = await this.sendWebSocketMessage({
|
|||
|
|
type: 'create_work_order',
|
|||
|
|
session_id: this.sessionId,
|
|||
|
|
title: title,
|
|||
|
|
description: description,
|
|||
|
|
category: category,
|
|||
|
|
priority: priority
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (response.type === 'work_order_created' && response.result.success) {
|
|||
|
|
const workOrderId = response.result.work_order_id;
|
|||
|
|
document.getElementById('work-order-id').value = workOrderId;
|
|||
|
|
this.addSystemMessage(`工单创建成功!工单号: ${response.result.order_id}`);
|
|||
|
|
|
|||
|
|
// 关闭模态框
|
|||
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('workOrderModal'));
|
|||
|
|
modal.hide();
|
|||
|
|
|
|||
|
|
// 清空表单
|
|||
|
|
document.getElementById('work-order-form').reset();
|
|||
|
|
|
|||
|
|
} else {
|
|||
|
|
this.showError('创建工单失败: ' + (response.result.error || '未知错误'));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('创建工单失败:', error);
|
|||
|
|
this.showError('创建工单失败: ' + error.message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
connectWebSocket() {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
try {
|
|||
|
|
this.websocket = new WebSocket('ws://localhost:8765');
|
|||
|
|
|
|||
|
|
// 设置连接超时
|
|||
|
|
const timeout = setTimeout(() => {
|
|||
|
|
if (this.websocket.readyState !== WebSocket.OPEN) {
|
|||
|
|
this.websocket.close();
|
|||
|
|
reject(new Error('WebSocket连接超时,请检查服务器是否启动'));
|
|||
|
|
}
|
|||
|
|
}, 5000); // 5秒超时
|
|||
|
|
|
|||
|
|
this.websocket.onopen = () => {
|
|||
|
|
clearTimeout(timeout);
|
|||
|
|
this.isConnected = true;
|
|||
|
|
this.updateConnectionStatus(true);
|
|||
|
|
resolve();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
this.websocket.onclose = () => {
|
|||
|
|
clearTimeout(timeout);
|
|||
|
|
this.isConnected = false;
|
|||
|
|
this.updateConnectionStatus(false);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
this.websocket.onerror = (error) => {
|
|||
|
|
clearTimeout(timeout);
|
|||
|
|
console.error('WebSocket错误:', error);
|
|||
|
|
reject(new Error('WebSocket连接失败,请检查服务器是否启动'));
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
this.websocket.onmessage = (event) => {
|
|||
|
|
try {
|
|||
|
|
const data = JSON.parse(event.data);
|
|||
|
|
this.handleWebSocketMessage(data);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('解析WebSocket消息失败:', error);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
reject(error);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
sendWebSocketMessage(message) {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) {
|
|||
|
|
reject(new Error('WebSocket未连接'));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const messageId = 'msg_' + Date.now();
|
|||
|
|
message.messageId = messageId;
|
|||
|
|
|
|||
|
|
// 设置超时
|
|||
|
|
const timeout = setTimeout(() => {
|
|||
|
|
reject(new Error('请求超时'));
|
|||
|
|
}, 10000);
|
|||
|
|
|
|||
|
|
// 监听响应
|
|||
|
|
const handleResponse = (event) => {
|
|||
|
|
try {
|
|||
|
|
const data = JSON.parse(event.data);
|
|||
|
|
if (data.messageId === messageId) {
|
|||
|
|
clearTimeout(timeout);
|
|||
|
|
this.websocket.removeEventListener('message', handleResponse);
|
|||
|
|
resolve(data);
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
// 忽略解析错误
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
this.websocket.addEventListener('message', handleResponse);
|
|||
|
|
this.websocket.send(JSON.stringify(message));
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
handleWebSocketMessage(data) {
|
|||
|
|
// 处理WebSocket消息
|
|||
|
|
console.log('收到WebSocket消息:', data);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
addMessage(role, content, metadata = {}) {
|
|||
|
|
const messagesContainer = document.getElementById('chat-messages');
|
|||
|
|
|
|||
|
|
// 如果是第一条消息,清空欢迎信息
|
|||
|
|
if (this.messageCount === 0) {
|
|||
|
|
messagesContainer.innerHTML = '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const messageDiv = document.createElement('div');
|
|||
|
|
messageDiv.className = `message ${role}`;
|
|||
|
|
|
|||
|
|
const avatar = document.createElement('div');
|
|||
|
|
avatar.className = 'message-avatar';
|
|||
|
|
avatar.textContent = role === 'user' ? 'U' : 'A';
|
|||
|
|
|
|||
|
|
const contentDiv = document.createElement('div');
|
|||
|
|
contentDiv.className = 'message-content';
|
|||
|
|
contentDiv.innerHTML = content;
|
|||
|
|
|
|||
|
|
// 添加时间戳
|
|||
|
|
const timeDiv = document.createElement('div');
|
|||
|
|
timeDiv.className = 'message-time';
|
|||
|
|
timeDiv.textContent = new Date().toLocaleTimeString();
|
|||
|
|
contentDiv.appendChild(timeDiv);
|
|||
|
|
|
|||
|
|
// 添加元数据
|
|||
|
|
if (metadata.knowledge_used && metadata.knowledge_used.length > 0) {
|
|||
|
|
const knowledgeDiv = document.createElement('div');
|
|||
|
|
knowledgeDiv.className = 'knowledge-info';
|
|||
|
|
knowledgeDiv.innerHTML = `<i class="fas fa-lightbulb me-1"></i>基于 ${metadata.knowledge_used.length} 条知识库信息生成`;
|
|||
|
|
contentDiv.appendChild(knowledgeDiv);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (metadata.confidence_score) {
|
|||
|
|
const confidenceDiv = document.createElement('div');
|
|||
|
|
confidenceDiv.className = 'confidence-score';
|
|||
|
|
confidenceDiv.textContent = `置信度: ${(metadata.confidence_score * 100).toFixed(1)}%`;
|
|||
|
|
contentDiv.appendChild(confidenceDiv);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (metadata.work_order_id) {
|
|||
|
|
const workOrderDiv = document.createElement('div');
|
|||
|
|
workOrderDiv.className = 'work-order-info';
|
|||
|
|
workOrderDiv.innerHTML = `<i class="fas fa-ticket-alt me-1"></i>关联工单: ${metadata.work_order_id}`;
|
|||
|
|
contentDiv.appendChild(workOrderDiv);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (role === 'user') {
|
|||
|
|
messageDiv.appendChild(contentDiv);
|
|||
|
|
messageDiv.appendChild(avatar);
|
|||
|
|
} else {
|
|||
|
|
messageDiv.appendChild(avatar);
|
|||
|
|
messageDiv.appendChild(contentDiv);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
messagesContainer.appendChild(messageDiv);
|
|||
|
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|||
|
|
|
|||
|
|
this.messageCount++;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
addSystemMessage(content) {
|
|||
|
|
const messagesContainer = document.getElementById('chat-messages');
|
|||
|
|
|
|||
|
|
const messageDiv = document.createElement('div');
|
|||
|
|
messageDiv.className = 'text-center text-muted py-2';
|
|||
|
|
messageDiv.innerHTML = `<small><i class="fas fa-info-circle me-1"></i>${content}</small>`;
|
|||
|
|
|
|||
|
|
messagesContainer.appendChild(messageDiv);
|
|||
|
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showTypingIndicator() {
|
|||
|
|
document.getElementById('typing-indicator').classList.add('show');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
hideTypingIndicator() {
|
|||
|
|
document.getElementById('typing-indicator').classList.remove('show');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateConnectionStatus(connected) {
|
|||
|
|
const statusElement = document.getElementById('connection-status');
|
|||
|
|
if (connected) {
|
|||
|
|
statusElement.className = 'connection-status connected';
|
|||
|
|
statusElement.innerHTML = '<i class="fas fa-circle me-1"></i>已连接';
|
|||
|
|
} else {
|
|||
|
|
statusElement.className = 'connection-status disconnected';
|
|||
|
|
statusElement.innerHTML = '<i class="fas fa-circle me-1"></i>未连接';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateSessionInfo() {
|
|||
|
|
const sessionInfo = document.getElementById('session-info');
|
|||
|
|
sessionInfo.innerHTML = `
|
|||
|
|
<div><strong>会话ID:</strong> ${this.sessionId}</div>
|
|||
|
|
<div><strong>消息数:</strong> ${this.messageCount}</div>
|
|||
|
|
<div><strong>状态:</strong> 活跃</div>
|
|||
|
|
`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
enableChat() {
|
|||
|
|
document.getElementById('start-chat').disabled = true;
|
|||
|
|
document.getElementById('end-chat').disabled = false;
|
|||
|
|
document.getElementById('message-input').disabled = false;
|
|||
|
|
document.getElementById('send-button').disabled = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
disableChat() {
|
|||
|
|
document.getElementById('start-chat').disabled = false;
|
|||
|
|
document.getElementById('end-chat').disabled = true;
|
|||
|
|
document.getElementById('message-input').disabled = true;
|
|||
|
|
document.getElementById('send-button').disabled = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showWorkOrderModal() {
|
|||
|
|
if (!this.sessionId) {
|
|||
|
|
this.showError('请先开始对话');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const modal = new bootstrap.Modal(document.getElementById('workOrderModal'));
|
|||
|
|
modal.show();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showError(message) {
|
|||
|
|
this.addSystemMessage(`<span class="text-danger">错误: ${message}</span>`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 初始化聊天客户端
|
|||
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|||
|
|
window.chatClient = new ChatClient();
|
|||
|
|
});
|