193 lines
8.9 KiB
HTML
193 lines
8.9 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Phone GUI Agent</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0a0a0a; color: #e0e0e0; height: 100vh; display: flex; flex-direction: column; }
|
||
header { padding: 12px 20px; background: #111; border-bottom: 1px solid #222; display: flex; align-items: center; gap: 12px; }
|
||
header h1 { font-size: 16px; font-weight: 600; }
|
||
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: #555; }
|
||
.status-dot.connected { background: #22c55e; }
|
||
.status-dot.running { background: #f59e0b; animation: pulse 1s infinite; }
|
||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
||
#device-info { font-size: 12px; color: #888; margin-left: auto; }
|
||
|
||
.main { flex: 1; display: flex; overflow: hidden; }
|
||
|
||
.panel-left { width: 320px; border-right: 1px solid #222; display: flex; flex-direction: column; }
|
||
.panel-center { flex: 1; display: flex; align-items: center; justify-content: center; background: #050505; }
|
||
.panel-right { width: 380px; border-left: 1px solid #222; display: flex; flex-direction: column; }
|
||
|
||
.phone-frame { width: 270px; height: 585px; border: 2px solid #333; border-radius: 24px; overflow: hidden; background: #111; position: relative; }
|
||
.phone-frame img { width: 100%; height: 100%; object-fit: contain; }
|
||
.phone-frame .placeholder { display: flex; align-items: center; justify-content: center; height: 100%; color: #444; font-size: 14px; }
|
||
|
||
.task-input { padding: 16px; border-bottom: 1px solid #222; }
|
||
.task-input textarea { width: 100%; height: 80px; background: #1a1a1a; border: 1px solid #333; border-radius: 8px; color: #e0e0e0; padding: 10px; font-size: 14px; resize: none; }
|
||
.task-input textarea:focus { outline: none; border-color: #4a9eff; }
|
||
.btn-row { display: flex; gap: 8px; margin-top: 8px; }
|
||
.btn { padding: 8px 16px; border-radius: 6px; border: none; cursor: pointer; font-size: 13px; font-weight: 500; }
|
||
.btn-primary { background: #4a9eff; color: #fff; }
|
||
.btn-primary:hover { background: #3a8eef; }
|
||
.btn-danger { background: #ef4444; color: #fff; }
|
||
.btn-secondary { background: #333; color: #ccc; }
|
||
|
||
.steps-list { flex: 1; overflow-y: auto; padding: 12px; }
|
||
.step-card { background: #1a1a1a; border: 1px solid #222; border-radius: 8px; padding: 12px; margin-bottom: 8px; font-size: 13px; }
|
||
.step-card .step-header { display: flex; justify-content: space-between; margin-bottom: 6px; }
|
||
.step-num { color: #4a9eff; font-weight: 600; }
|
||
.step-action { color: #22c55e; font-family: monospace; }
|
||
.step-action.error { color: #ef4444; }
|
||
.step-obs { color: #999; margin-top: 4px; }
|
||
.step-think { color: #f59e0b; margin-top: 4px; font-style: italic; }
|
||
|
||
.log-panel { flex: 1; overflow-y: auto; padding: 12px; }
|
||
.log-panel h3 { font-size: 13px; color: #888; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 1px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<div class="status-dot" id="statusDot"></div>
|
||
<h1>Phone GUI Agent</h1>
|
||
<span id="device-info">检测设备中...</span>
|
||
</header>
|
||
|
||
<div class="main">
|
||
<div class="panel-left">
|
||
<div class="task-input">
|
||
<textarea id="taskInput" placeholder="输入任务指令,例如: 打开设置,连接WiFi 打开微信,搜索张三发消息"></textarea>
|
||
<div class="btn-row">
|
||
<button class="btn btn-primary" id="btnRun" onclick="runTask()">执行任务</button>
|
||
<button class="btn btn-danger" id="btnStop" onclick="stopTask()" style="display:none">停止</button>
|
||
<button class="btn btn-secondary" onclick="refreshScreenshot()">截屏</button>
|
||
</div>
|
||
</div>
|
||
<div class="steps-list" id="stepsList"></div>
|
||
</div>
|
||
|
||
<div class="panel-center">
|
||
<div class="phone-frame">
|
||
<img id="phoneScreen" style="display:none" />
|
||
<div class="placeholder" id="phonePlaceholder">连接设备后显示截图</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel-right">
|
||
<div class="log-panel">
|
||
<h3>Agent 思考过程</h3>
|
||
<div id="thinkingLog"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let ws = null;
|
||
|
||
async function checkDevice() {
|
||
try {
|
||
const resp = await fetch('/api/device');
|
||
const data = await resp.json();
|
||
const dot = document.getElementById('statusDot');
|
||
const info = document.getElementById('device-info');
|
||
if (data.connected) {
|
||
dot.className = 'status-dot connected';
|
||
info.textContent = `${data.model} (${data.resolution}) - ${data.serial}`;
|
||
refreshScreenshot();
|
||
} else {
|
||
dot.className = 'status-dot';
|
||
info.textContent = data.error || '未连接设备';
|
||
}
|
||
} catch (e) {
|
||
document.getElementById('device-info').textContent = '服务未启动';
|
||
}
|
||
}
|
||
|
||
async function refreshScreenshot() {
|
||
try {
|
||
const resp = await fetch('/api/screenshot');
|
||
const data = await resp.json();
|
||
if (data.ok) {
|
||
const img = document.getElementById('phoneScreen');
|
||
img.src = 'data:image/png;base64,' + data.image;
|
||
img.style.display = 'block';
|
||
document.getElementById('phonePlaceholder').style.display = 'none';
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
|
||
function runTask() {
|
||
const task = document.getElementById('taskInput').value.trim();
|
||
if (!task) return;
|
||
|
||
document.getElementById('stepsList').innerHTML = '';
|
||
document.getElementById('thinkingLog').innerHTML = '';
|
||
document.getElementById('btnRun').style.display = 'none';
|
||
document.getElementById('btnStop').style.display = 'inline-block';
|
||
document.getElementById('statusDot').className = 'status-dot running';
|
||
|
||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||
ws = new WebSocket(`${protocol}//${location.host}/ws/task`);
|
||
|
||
ws.onopen = () => {
|
||
ws.send(JSON.stringify({ task }));
|
||
};
|
||
|
||
ws.onmessage = (e) => {
|
||
const data = JSON.parse(e.data);
|
||
if (data.status === 'step') {
|
||
addStep(data);
|
||
} else if (data.status === 'completed' || data.status === 'failed' || data.status === 'stopped') {
|
||
taskDone(data);
|
||
}
|
||
};
|
||
|
||
ws.onclose = () => taskDone({ status: 'disconnected' });
|
||
}
|
||
|
||
function addStep(data) {
|
||
const list = document.getElementById('stepsList');
|
||
const card = document.createElement('div');
|
||
card.className = 'step-card';
|
||
card.innerHTML = `
|
||
<div class="step-header">
|
||
<span class="step-num">Step ${data.step}</span>
|
||
<span class="step-action ${data.error ? 'error' : ''}">${data.error || data.action_desc || data.action_type}</span>
|
||
</div>
|
||
${data.observation ? `<div class="step-obs">${data.observation}</div>` : ''}
|
||
${data.thinking ? `<div class="step-think">${data.thinking}</div>` : ''}
|
||
`;
|
||
list.appendChild(card);
|
||
list.scrollTop = list.scrollHeight;
|
||
|
||
if (data.thinking) {
|
||
const log = document.getElementById('thinkingLog');
|
||
const p = document.createElement('div');
|
||
p.className = 'step-card';
|
||
p.innerHTML = `<span class="step-num">Step ${data.step}</span>: ${data.thinking}`;
|
||
log.appendChild(p);
|
||
log.scrollTop = log.scrollHeight;
|
||
}
|
||
|
||
refreshScreenshot();
|
||
}
|
||
|
||
function taskDone(data) {
|
||
document.getElementById('btnRun').style.display = 'inline-block';
|
||
document.getElementById('btnStop').style.display = 'none';
|
||
document.getElementById('statusDot').className = 'status-dot connected';
|
||
if (ws) { ws.close(); ws = null; }
|
||
}
|
||
|
||
async function stopTask() {
|
||
await fetch('/api/stop', { method: 'POST' });
|
||
}
|
||
|
||
checkDevice();
|
||
setInterval(checkDevice, 10000);
|
||
</script>
|
||
</body>
|
||
</html>
|