multica/apps/server/public/client.html
Naiyuan Qing 6ef58a0cab refactor: restructure to monorepo architecture
- Move core agent engine to packages/core/
- Add packages/types/ for shared TypeScript types
- Add packages/utils/ for utility functions
- Add apps/cli/ for command-line interface
- Add apps/gateway/ for NestJS WebSocket gateway
- Add apps/server/ for REST API server
- Restructure desktop app (electron/ → src/main/, src/preload/)
- Update pnpm workspace configuration
- Remove legacy src/ directory

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-10 18:00:23 +08:00

211 lines
9 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo Client</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace; background: #0a0a0a; color: #e0e0e0; padding: 2rem; }
h1 { font-size: 1.4rem; margin-bottom: 0.3rem; color: #fff; }
.badge { display: inline-block; font-size: 0.7rem; background: #3a2a00; color: #ffaa33; border: 1px solid #554400; border-radius: 3px; padding: 0.1rem 0.4rem; margin-left: 0.5rem; vertical-align: middle; }
.subtitle { color: #555; font-size: 0.8rem; margin-bottom: 1.5rem; }
.card { background: #161616; border: 1px solid #2a2a2a; border-radius: 8px; padding: 1.2rem; margin-bottom: 1.2rem; }
label { display: block; color: #888; font-size: 0.8rem; margin-bottom: 0.3rem; }
input { width: 100%; background: #0e0e0e; border: 1px solid #333; border-radius: 4px; padding: 0.5rem 0.7rem; color: #e0e0e0; font-family: monospace; font-size: 0.85rem; margin-bottom: 0.8rem; }
input:focus { outline: none; border-color: #555; }
input:disabled { opacity: 0.5; }
button { background: #2a2a2a; color: #e0e0e0; border: 1px solid #444; border-radius: 4px; padding: 0.5rem 1rem; cursor: pointer; font-size: 0.85rem; }
button:hover { background: #3a3a3a; }
button:disabled { opacity: 0.4; cursor: not-allowed; }
.btn-connect { background: #1a3a1a; border-color: #2a5a2a; }
.btn-connect:hover { background: #2a4a2a; }
.btn-disconnect { background: #3a1a1a; border-color: #5a2a2a; }
.btn-disconnect:hover { background: #4a2a2a; }
.btn-send { background: #1a2a3a; border-color: #2a4a5a; }
.btn-send:hover { background: #2a3a4a; }
.status { font-size: 0.8rem; margin-bottom: 0.8rem; }
.status .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 0.4rem; vertical-align: middle; }
.dot-off { background: #555; }
.dot-on { background: #4a4; }
.dot-connecting { background: #aa4; }
.device-id { font-size: 0.75rem; color: #555; margin-bottom: 1rem; word-break: break-all; }
.chat { display: none; }
.chat.active { display: block; }
.messages { background: #0e0e0e; border: 1px solid #222; border-radius: 4px; height: 300px; overflow-y: auto; padding: 0.6rem; margin-bottom: 0.8rem; font-size: 0.8rem; font-family: monospace; }
.messages .msg { padding: 0.3rem 0; border-bottom: 1px solid #1a1a1a; }
.messages .msg:last-child { border-bottom: none; }
.msg-from { color: #888; }
.msg-self { color: #6a8faa; }
.msg-in { color: #6aaa6a; }
.msg-system { color: #aa8833; font-style: italic; }
.send-row { display: flex; gap: 0.5rem; }
.send-row input { margin-bottom: 0; flex: 1; }
</style>
</head>
<body>
<h1>Demo Client <span class="badge">DEMO</span></h1>
<p class="subtitle">Test client for sending messages to agents via Gateway</p>
<div class="card">
<div class="device-id">Device ID: <span id="device-id"></span></div>
<div class="status">
Status: <span class="dot dot-off" id="status-dot"></span><span id="status-text">Disconnected</span>
</div>
<div id="connect-form">
<label>Gateway URL</label>
<input type="text" id="gateway-url" placeholder="http://localhost:3000" />
<button class="btn-connect" id="btn-connect" onclick="doConnect()">Connect</button>
</div>
<div id="connected-form" style="display:none;">
<button class="btn-disconnect" onclick="doDisconnect()">Disconnect</button>
</div>
</div>
<div class="card chat" id="chat">
<label>Messages</label>
<div class="messages" id="messages"></div>
<div style="display:flex; gap:0.5rem; margin-bottom:0.5rem;">
<div style="flex:1;">
<label>Target Device ID</label>
<input type="text" id="target-device-id" placeholder="Hub device ID" />
</div>
<div style="flex:1;">
<label>Agent ID</label>
<input type="text" id="target-agent-id" placeholder="Agent ID on that device" />
</div>
</div>
<div class="send-row">
<input type="text" id="msg-input" placeholder="Type a message..." onkeydown="if(event.key==='Enter')doSend()" />
<button class="btn-send" onclick="doSend()">Send</button>
</div>
</div>
<script src="https://cdn.socket.io/4.8.3/socket.io.min.js"></script>
<script>
// Device ID: persist in localStorage
const STORAGE_KEY = 'demo-client-device-id';
let deviceId = localStorage.getItem(STORAGE_KEY);
if (!deviceId) {
// Simple UUID v4 for demo (no uuid lib in browser)
deviceId = crypto.randomUUID();
localStorage.setItem(STORAGE_KEY, deviceId);
}
document.getElementById('device-id').textContent = deviceId;
// Restore last used values
document.getElementById('gateway-url').value = localStorage.getItem('demo-client-gateway-url') || 'http://localhost:3000';
let socket = null;
function setStatus(state) {
const dot = document.getElementById('status-dot');
const text = document.getElementById('status-text');
dot.className = 'dot ' + ({ disconnected: 'dot-off', connecting: 'dot-connecting', connected: 'dot-on', registered: 'dot-on' }[state] || 'dot-off');
text.textContent = state.charAt(0).toUpperCase() + state.slice(1);
}
function appendMsg(type, text) {
const el = document.getElementById('messages');
const div = document.createElement('div');
div.className = 'msg msg-' + type;
div.textContent = text;
el.appendChild(div);
el.scrollTop = el.scrollHeight;
}
function doConnect() {
const url = document.getElementById('gateway-url').value.trim();
if (!url) { alert('Please fill in Gateway URL'); return; }
localStorage.setItem('demo-client-gateway-url', url);
setStatus('connecting');
document.getElementById('btn-connect').disabled = true;
socket = io(url, {
path: '/ws',
query: { deviceId, deviceType: 'client' },
reconnection: true,
reconnectionDelay: 1000,
});
socket.on('connect', () => {
setStatus('connected');
// 服务端从 query 自动注册,等待 registered 事件
});
socket.on('registered', (res) => {
if (res.success) {
setStatus('registered');
document.getElementById('connect-form').style.display = 'none';
document.getElementById('connected-form').style.display = 'block';
document.getElementById('chat').classList.add('active');
document.getElementById('target-device-id').value = localStorage.getItem('demo-client-target-device-id') || '';
document.getElementById('target-agent-id').value = localStorage.getItem('demo-client-target-agent-id') || '';
appendMsg('system', `Connected as ${deviceId}`);
} else {
appendMsg('system', `Registration failed: ${res.error}`);
setStatus('disconnected');
document.getElementById('btn-connect').disabled = false;
}
});
socket.on('receive', (msg) => {
appendMsg('in', `[${msg.from}] ${typeof msg.payload === 'string' ? msg.payload : JSON.stringify(msg.payload)}`);
});
socket.on('send_error', (err) => {
appendMsg('system', `Send error: ${err.error} (${err.code})`);
});
socket.on('disconnect', (reason) => {
setStatus('disconnected');
appendMsg('system', `Disconnected: ${reason}`);
});
socket.on('connect_error', (err) => {
appendMsg('system', `Connection error: ${err.message}`);
setStatus('disconnected');
document.getElementById('btn-connect').disabled = false;
});
}
function doDisconnect() {
if (socket) { socket.disconnect(); socket = null; }
setStatus('disconnected');
document.getElementById('connect-form').style.display = 'block';
document.getElementById('connected-form').style.display = 'none';
document.getElementById('chat').classList.remove('active');
document.getElementById('btn-connect').disabled = false;
}
function doSend() {
const input = document.getElementById('msg-input');
const text = input.value.trim();
const targetDeviceId = document.getElementById('target-device-id').value.trim();
const targetAgentId = document.getElementById('target-agent-id').value.trim();
if (!text || !socket) return;
if (!targetDeviceId || !targetAgentId) { alert('Please fill in Target Device ID and Agent ID'); return; }
localStorage.setItem('demo-client-target-device-id', targetDeviceId);
localStorage.setItem('demo-client-target-agent-id', targetAgentId);
const msgId = crypto.randomUUID();
socket.emit('send', {
id: msgId,
uid: null,
from: deviceId,
to: targetDeviceId,
action: 'message',
payload: { agentId: targetAgentId, content: text },
});
appendMsg('self', `[you -> ${targetDeviceId}/${targetAgentId}] ${text}`);
input.value = '';
}
</script>
</body>
</html>