- Python FastAPI 引擎:世界状态模拟、全局能量条、世界事件系统 - Node.js WebSocket 服务器:实时通信、事件队列批处理 - 前端仪表盘:世界状态可视化、行动日志、事件展示 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
250 lines
10 KiB
HTML
250 lines
10 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>AI Town</title>
|
|
<style>
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
color: #e4e4e7;
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
.container { max-width: 900px; margin: 0 auto; }
|
|
header { text-align: center; margin-bottom: 20px; }
|
|
header h1 {
|
|
font-size: 28px;
|
|
background: linear-gradient(90deg, #60a5fa, #a78bfa);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
.status-bar { display: flex; align-items: center; justify-content: center; gap: 8px; margin-top: 8px; font-size: 13px; color: #71717a; }
|
|
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: #ef4444; }
|
|
.status-dot.connected { background: #22c55e; }
|
|
|
|
/* 输入区域 */
|
|
.input-section { background: rgba(255,255,255,0.05); border-radius: 12px; padding: 16px; margin-bottom: 16px; }
|
|
.input-row { display: flex; gap: 8px; margin-bottom: 10px; }
|
|
.input-row input { flex: 1; padding: 10px 14px; border: 1px solid #3f3f46; border-radius: 8px; background: #27272a; color: #e4e4e7; font-size: 14px; outline: none; }
|
|
.input-row input:focus { border-color: #60a5fa; }
|
|
button { padding: 10px 18px; border: none; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; }
|
|
.btn-primary { background: #3b82f6; color: white; }
|
|
.btn-primary:hover { background: #2563eb; }
|
|
.quick-btns { display: flex; gap: 8px; }
|
|
.btn-quick { background: #3f3f46; color: #a1a1aa; }
|
|
.btn-quick:hover { background: #52525b; color: #e4e4e7; }
|
|
|
|
/* 能量条 - 极简 */
|
|
#global-meter { width: 100%; height: 16px; background: #27272a; border-radius: 8px; margin-bottom: 16px; overflow: hidden; }
|
|
#meter-bar { height: 100%; width: 0%; background: linear-gradient(90deg, #00ffcc, #ff0066); transition: width 0.3s ease; }
|
|
|
|
/* 世界事件提示 */
|
|
#world-event { background: #422006; border: 1px solid #f59e0b; border-radius: 8px; padding: 12px; margin-bottom: 16px; display: none; font-size: 14px; }
|
|
|
|
/* 持续影响 */
|
|
#world-effects { margin-bottom: 16px; }
|
|
.effect-item { display: inline-block; background: #1e3a5f; border-radius: 6px; padding: 6px 10px; margin-right: 8px; margin-bottom: 8px; font-size: 12px; }
|
|
.effect-item .remaining { color: #f59e0b; margin-left: 4px; }
|
|
|
|
/* 主内容区 */
|
|
.main-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
|
@media (max-width: 700px) { .main-grid { grid-template-columns: 1fr; } }
|
|
.card { background: rgba(255,255,255,0.05); border-radius: 12px; padding: 16px; }
|
|
.card-title { font-size: 12px; font-weight: 600; color: #71717a; text-transform: uppercase; margin-bottom: 12px; }
|
|
|
|
/* 世界状态 */
|
|
.world-stats { display: flex; gap: 20px; }
|
|
.stat-value { font-size: 24px; font-weight: 600; color: #60a5fa; }
|
|
.stat-label { font-size: 12px; color: #71717a; }
|
|
|
|
/* 角色 */
|
|
.agent { background: #27272a; border-radius: 8px; padding: 12px; margin-bottom: 8px; }
|
|
.agent:last-child { margin-bottom: 0; }
|
|
.agent-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
|
.agent-name { font-weight: 600; }
|
|
.agent-emotion { font-size: 12px; padding: 2px 8px; border-radius: 10px; background: #3f3f46; }
|
|
.emotion-happy { background: #166534; color: #86efac; }
|
|
.emotion-anxious { background: #7f1d1d; color: #fca5a5; }
|
|
.emotion-calm { background: #1e3a5f; color: #93c5fd; }
|
|
.agent-info { font-size: 13px; color: #a1a1aa; }
|
|
|
|
/* 行动日志 */
|
|
.actions-list { max-height: 300px; overflow-y: auto; }
|
|
.action-item { padding: 8px; background: #27272a; border-radius: 6px; margin-bottom: 6px; font-size: 13px; }
|
|
.action-agent { font-weight: 600; color: #a78bfa; }
|
|
.action-say { color: #fbbf24; }
|
|
.action-do { color: #34d399; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<header>
|
|
<h1>AI Town</h1>
|
|
<div class="status-bar">
|
|
<span class="status-dot" id="statusDot"></span>
|
|
<span id="statusText">未连接</span>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- 输入区域 -->
|
|
<section class="input-section">
|
|
<div class="input-row">
|
|
<input type="text" id="msgInput" placeholder="输入消息..." />
|
|
<button class="btn-primary" onclick="sendMessage()">发送</button>
|
|
</div>
|
|
<div class="quick-btns">
|
|
<button class="btn-quick" onclick="sendQuick('支持')">支持</button>
|
|
<button class="btn-quick" onclick="sendQuick('混乱')">混乱</button>
|
|
<button class="btn-quick" onclick="sendQuick('下雨')">下雨</button>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ① 能量条 -->
|
|
<div id="global-meter"><div id="meter-bar"></div></div>
|
|
|
|
<!-- ② 世界事件提示 -->
|
|
<div id="world-event"></div>
|
|
|
|
<!-- ③ 持续影响 -->
|
|
<div id="world-effects"></div>
|
|
|
|
<!-- 主内容区 -->
|
|
<div class="main-grid">
|
|
<div>
|
|
<div class="card" style="margin-bottom: 16px;">
|
|
<div class="card-title">世界状态</div>
|
|
<div class="world-stats">
|
|
<div><div class="stat-value" id="tick">0</div><div class="stat-label">Tick</div></div>
|
|
<div><div class="stat-value" id="weather">-</div><div class="stat-label">天气</div></div>
|
|
<div><div class="stat-value" id="mood">0</div><div class="stat-label">情绪值</div></div>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-title">居民</div>
|
|
<div id="agents"></div>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-title">行动日志</div>
|
|
<div class="actions-list" id="actions"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// WebSocket
|
|
let ws = null;
|
|
let reconnectDelay = 1000;
|
|
|
|
function connect() {
|
|
ws = new WebSocket('ws://localhost:3000');
|
|
ws.onopen = () => {
|
|
document.getElementById('statusDot').classList.add('connected');
|
|
document.getElementById('statusText').textContent = '已连接';
|
|
reconnectDelay = 1000;
|
|
};
|
|
ws.onclose = () => {
|
|
document.getElementById('statusDot').classList.remove('connected');
|
|
document.getElementById('statusText').textContent = '重连中...';
|
|
setTimeout(connect, reconnectDelay);
|
|
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
|
|
};
|
|
ws.onerror = () => ws.close();
|
|
ws.onmessage = (e) => updateUI(JSON.parse(e.data));
|
|
}
|
|
|
|
// 发送消息
|
|
function sendMessage() {
|
|
const input = document.getElementById('msgInput');
|
|
const text = input.value.trim();
|
|
if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;
|
|
ws.send(JSON.stringify({ type: 'comment', text, user: 'viewer' }));
|
|
input.value = '';
|
|
}
|
|
|
|
function sendQuick(text) {
|
|
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
ws.send(JSON.stringify({ type: 'comment', text, user: 'viewer' }));
|
|
}
|
|
|
|
// 更新 UI
|
|
const weatherMap = { sunny: '☀️', rainy: '🌧️' };
|
|
const actionHistory = [];
|
|
|
|
function updateUI(data) {
|
|
const { world_state, actions, global_event } = data;
|
|
|
|
// 世界状态
|
|
document.getElementById('tick').textContent = world_state.tick;
|
|
document.getElementById('weather').textContent = weatherMap[world_state.weather] || world_state.weather;
|
|
document.getElementById('mood').textContent = world_state.town_mood;
|
|
|
|
// ① 能量条
|
|
if (world_state.global_meter) {
|
|
const meter = world_state.global_meter;
|
|
const percent = Math.min(100, meter.value / meter.threshold * 100);
|
|
document.getElementById('meter-bar').style.width = percent + '%';
|
|
}
|
|
|
|
// ② 世界事件
|
|
const eventEl = document.getElementById('world-event');
|
|
if (global_event?.triggered && global_event.event) {
|
|
eventEl.textContent = '🌍 世界事件:' + global_event.event.description;
|
|
eventEl.style.display = 'block';
|
|
} else {
|
|
eventEl.style.display = 'none';
|
|
}
|
|
|
|
// ③ 持续影响
|
|
const effectsEl = document.getElementById('world-effects');
|
|
if (world_state.world_effects?.length > 0) {
|
|
effectsEl.innerHTML = world_state.world_effects.map(e =>
|
|
`<span class="effect-item">${e.name}<span class="remaining">(${e.remaining_ticks})</span></span>`
|
|
).join('');
|
|
} else {
|
|
effectsEl.innerHTML = '';
|
|
}
|
|
|
|
// 角色
|
|
const agentsEl = document.getElementById('agents');
|
|
agentsEl.innerHTML = '';
|
|
for (const [id, agent] of Object.entries(world_state.agents)) {
|
|
agentsEl.innerHTML += `
|
|
<div class="agent">
|
|
<div class="agent-header">
|
|
<span class="agent-name">${id}</span>
|
|
<span class="agent-emotion emotion-${agent.emotion}">${agent.emotion}</span>
|
|
</div>
|
|
<div class="agent-info">记忆: ${agent.memory.slice(-2).join(' | ') || '-'}</div>
|
|
</div>`;
|
|
}
|
|
|
|
// 行动日志(倒序:新的在上)
|
|
const now = new Date().toLocaleString('zh-CN', {
|
|
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
|
|
}).replace(/\//g, '-');
|
|
for (const a of actions) {
|
|
a.time = now;
|
|
actionHistory.unshift(a);
|
|
}
|
|
if (actionHistory.length > 50) actionHistory.length = 50;
|
|
document.getElementById('actions').innerHTML = actionHistory.map(a =>
|
|
`<div class="action-item"><span style="color:#71717a;font-size:11px;">${a.time}</span> <span class="action-agent">${a.agent_id}</span> <span class="action-say">「${a.say}」</span> <span class="action-do">→ ${a.do}</span></div>`
|
|
).join('');
|
|
}
|
|
|
|
// 回车发送
|
|
document.getElementById('msgInput').addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') sendMessage();
|
|
});
|
|
|
|
// 启动
|
|
connect();
|
|
</script>
|
|
</body>
|
|
</html>
|