Files
gui-agent/web/templates/index.html

193 lines
8.9 KiB
HTML
Raw Permalink 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>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="输入任务指令,例如:&#10;打开设置连接WiFi&#10;打开微信,搜索张三发消息"></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>