Files
my_one_web/templates/chat/index.html
小白 db5378c7e8 feat: 添加聊天会话界面
- 新增 OpenClaw Gateway 连接器 (api/openclaw_connector.py)
- 新增聊天 WebSocket 路由 (api/chat.py)
- 新增聊天界面模板 (templates/chat/index.html)
- 新增聊天样式 (static/css/chat.css)
- 修改 app.py 支持 SocketIO
- 登录后默认跳转到聊天界面

作者:小白 🐶
2026-03-14 06:12:28 +08:00

322 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>