auto-save 2026-04-01 09:03 (+8, ~2)
This commit is contained in:
192
web/templates/index.html
Normal file
192
web/templates/index.html
Normal file
@@ -0,0 +1,192 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user