Files
my_one_web/templates/chat/index.html

322 lines
12 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>小白聊天 - OpenClaw</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/chat.css') }}">
</head>
<body class="chat-body">
<div class="chat-container">
<!-- 顶部导航栏 -->
<div class="chat-header">
<div class="header-left">
<span class="logo">🐶 小白</span>
<select id="gateway-select" class="gateway-selector">
<option value="local">本地 OpenClaw</option>
</select>
</div>
<div class="header-right">
<span id="connection-status" class="status-badge connecting">连接中...</span>
<span class="user-info">{{ username }}</span>
<a href="{{ url_for('logout') }}" class="logout-btn">退出</a>
</div>
</div>
<!-- 消息列表区域 -->
<div id="message-list" class="message-list">
<div class="welcome-message">
<div class="welcome-avatar">🐶</div>
<div class="welcome-text">
<h3>你好,{{ username }}</h3>
<p>我是小白,你的 AI 助手。有什么可以帮你的吗?</p>
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="chat-input-area">
<div class="input-wrapper">
<textarea
id="message-input"
placeholder="输入消息... (Enter 发送, Shift+Enter 换行)"
rows="1"
autofocus
></textarea>
<button id="send-btn" onclick="sendMessage()" disabled>
<span>发送</span>
</button>
</div>
<div class="input-hint">
<span id="typing-indicator" class="typing-indicator" style="display: none;">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
小白正在思考...
</span>
</div>
</div>
</div>
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<script>
// 全局变量
let socket = null;
let currentGateway = 'local';
let isConnected = false;
let userId = null;
let messageHistory = [];
// DOM 元素
const messageList = document.getElementById('message-list');
const messageInput = document.getElementById('message-input');
const sendBtn = document.getElementById('send-btn');
const gatewaySelect = document.getElementById('gateway-select');
const connectionStatus = document.getElementById('connection-status');
const typingIndicator = document.getElementById('typing-indicator');
// 初始化
document.addEventListener('DOMContentLoaded', initChat);
function initChat() {
// 连接 Socket.IO
socket = io('/chat', {
transports: ['websocket', 'polling']
});
// 事件监听
socket.on('connect', () => {
console.log('[Socket] Connected');
});
socket.on('connected', (data) => {
console.log('[Chat] Connected:', data);
userId = data.userId;
isConnected = true;
updateConnectionStatus(true);
// 更新 Gateway 列表
gatewaySelect.innerHTML = data.gateways.map(g =>
`<option value="${g}">${g}</option>`
).join('');
// 更新状态
updateGatewayStatus(data.status);
});
socket.on('disconnect', () => {
console.log('[Socket] Disconnected');
isConnected = false;
updateConnectionStatus(false);
});
socket.on('message_sent', (data) => {
console.log('[Chat] Message sent:', data);
addMessage('user', data.message, data.timestamp);
showTypingIndicator();
});
socket.on('agent_response', (data) => {
console.log('[Chat] Agent response:', data);
hideTypingIndicator();
handleAgentResponse(data);
});
socket.on('error', (data) => {
console.error('[Chat] Error:', data);
hideTypingIndicator();
addMessage('system', `❌ ${data.message}`, Date.now());
});
socket.on('gateway_changed', (data) => {
console.log('[Chat] Gateway changed:', data);
currentGateway = data.gateway;
updateConnectionStatus(data.connected);
});
socket.on('status_update', (data) => {
updateGatewayStatus(data.status);
});
// 输入框事件
messageInput.addEventListener('input', handleInputChange);
messageInput.addEventListener('keydown', handleKeyDown);
// Gateway 切换
gatewaySelect.addEventListener('change', (e) => {
currentGateway = e.target.value;
socket.emit('switch_gateway', { gateway: currentGateway });
});
}
function handleInputChange() {
const hasContent = messageInput.value.trim().length > 0;
sendBtn.disabled = !hasContent || !isConnected;
// 自动调整高度
messageInput.style.height = 'auto';
messageInput.style.height = Math.min(messageInput.scrollHeight, 150) + 'px';
}
function handleKeyDown(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}
function sendMessage() {
const message = messageInput.value.trim();
if (!message || !isConnected) return;
socket.emit('send_message', {
gateway: currentGateway,
message: message
});
messageInput.value = '';
handleInputChange();
}
function addMessage(type, content, timestamp) {
const div = document.createElement('div');
div.className = `message ${type}`;
const time = new Date(timestamp).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
});
const avatar = type === 'user' ? '👤' : '🐶';
const sender = type === 'user' ? '我' : '小白';
div.innerHTML = `
<div class="message-avatar">${avatar}</div>
<div class="message-content">
<div class="message-header">
<span class="message-sender">${sender}</span>
<span class="message-time">${time}</span>
</div>
<div class="message-text">${escapeHtml(content)}</div>
</div>
`;
messageList.appendChild(div);
scrollToBottom();
// 保存历史
messageHistory.push({ type, content, timestamp });
}
function handleAgentResponse(data) {
const responseData = data.data || {};
// 尝试提取文本内容
let content = '';
if (responseData.result) {
content = responseData.result;
} else if (responseData.content) {
content = responseData.content;
} else if (responseData.params?.result) {
content = responseData.params.result;
} else if (responseData.error) {
content = `错误: ${responseData.error}`;
} else {
// 显示原始响应
content = JSON.stringify(responseData, null, 2);
}
if (content) {
addMessage('agent', content, data.timestamp || Date.now());
}
}
function addMessage(type, content, timestamp) {
const div = document.createElement('div');
div.className = `message ${type}`;
const time = new Date(timestamp).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
});
const avatar = type === 'user' ? '👤' : (type === 'agent' ? '🐶' : '⚠️');
const sender = type === 'user' ? '我' : (type === 'agent' ? '小白' : '系统');
// 处理 markdown 代码块
let displayContent = escapeHtml(content);
if (type === 'agent') {
displayContent = formatContent(content);
}
div.innerHTML = `
<div class="message-avatar">${avatar}</div>
<div class="message-content">
<div class="message-header">
<span class="message-sender">${sender}</span>
<span class="message-time">${time}</span>
</div>
<div class="message-text">${displayContent}</div>
</div>
`;
messageList.appendChild(div);
scrollToBottom();
}
function formatContent(content) {
// 简单的 markdown 处理
let html = escapeHtml(content);
// 代码块
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code class="language-$1">$2</code></pre>');
// 行内代码
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
// 粗体
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
// 换行
html = html.replace(/\n/g, '<br>');
return html;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function scrollToBottom() {
messageList.scrollTop = messageList.scrollHeight;
}
function updateConnectionStatus(connected) {
if (connected) {
connectionStatus.className = 'status-badge connected';
connectionStatus.textContent = '已连接';
} else {
connectionStatus.className = 'status-badge disconnected';
connectionStatus.textContent = '未连接';
}
sendBtn.disabled = !connected || !messageInput.value.trim();
}
function updateGatewayStatus(status) {
// 可以显示各 Gateway 的连接状态
console.log('[Chat] Gateway status:', status);
}
function showTypingIndicator() {
typingIndicator.style.display = 'inline-flex';
}
function hideTypingIndicator() {
typingIndicator.style.display = 'none';
}
</script>
</body>
</html>