821 lines
27 KiB
HTML
821 lines
27 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>GOAP Debug Visualizer - VillSim</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
|
<style>
|
|
:root {
|
|
--bg-primary: #0d1117;
|
|
--bg-secondary: #161b22;
|
|
--bg-tertiary: #21262d;
|
|
--border-color: #30363d;
|
|
--text-primary: #e6edf3;
|
|
--text-secondary: #8b949e;
|
|
--text-muted: #6e7681;
|
|
--accent-blue: #58a6ff;
|
|
--accent-green: #3fb950;
|
|
--accent-orange: #d29922;
|
|
--accent-red: #f85149;
|
|
--accent-purple: #a371f7;
|
|
--accent-cyan: #39c5cf;
|
|
}
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.header {
|
|
background: var(--bg-secondary);
|
|
border-bottom: 1px solid var(--border-color);
|
|
padding: 16px 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
color: var(--accent-cyan);
|
|
}
|
|
|
|
.header-controls {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: center;
|
|
}
|
|
|
|
.btn {
|
|
padding: 8px 16px;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 6px;
|
|
background: var(--bg-tertiary);
|
|
color: var(--text-primary);
|
|
font-family: inherit;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.btn:hover {
|
|
background: var(--border-color);
|
|
border-color: var(--text-muted);
|
|
}
|
|
|
|
.btn-primary {
|
|
background: var(--accent-blue);
|
|
border-color: var(--accent-blue);
|
|
color: #fff;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background: #4c9aff;
|
|
}
|
|
|
|
.status-badge {
|
|
padding: 4px 12px;
|
|
border-radius: 20px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.status-connected {
|
|
background: rgba(63, 185, 80, 0.2);
|
|
color: var(--accent-green);
|
|
}
|
|
|
|
.status-disconnected {
|
|
background: rgba(248, 81, 73, 0.2);
|
|
color: var(--accent-red);
|
|
}
|
|
|
|
.main-content {
|
|
display: grid;
|
|
grid-template-columns: 280px 1fr 400px;
|
|
height: calc(100vh - 65px);
|
|
}
|
|
|
|
.panel {
|
|
background: var(--bg-secondary);
|
|
border-right: 1px solid var(--border-color);
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.panel:last-child {
|
|
border-right: none;
|
|
border-left: 1px solid var(--border-color);
|
|
}
|
|
|
|
.panel-header {
|
|
padding: 16px;
|
|
border-bottom: 1px solid var(--border-color);
|
|
background: var(--bg-tertiary);
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 10;
|
|
}
|
|
|
|
.panel-header h2 {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.agent-list {
|
|
padding: 8px;
|
|
}
|
|
|
|
.agent-item {
|
|
padding: 12px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
margin-bottom: 4px;
|
|
transition: background 0.15s ease;
|
|
}
|
|
|
|
.agent-item:hover {
|
|
background: var(--bg-tertiary);
|
|
}
|
|
|
|
.agent-item.selected {
|
|
background: rgba(88, 166, 255, 0.15);
|
|
border: 1px solid var(--accent-blue);
|
|
}
|
|
|
|
.agent-item .agent-name {
|
|
font-weight: 500;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.agent-item .agent-action {
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
font-family: 'IBM Plex Mono', monospace;
|
|
}
|
|
|
|
.agent-item .agent-goal {
|
|
font-size: 11px;
|
|
color: var(--accent-cyan);
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.center-panel {
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--bg-primary);
|
|
}
|
|
|
|
.plan-view {
|
|
padding: 24px;
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.plan-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.plan-header h2 {
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.plan-goal-badge {
|
|
padding: 6px 16px;
|
|
background: rgba(163, 113, 247, 0.2);
|
|
color: var(--accent-purple);
|
|
border-radius: 20px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.world-state-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 12px;
|
|
margin-bottom: 32px;
|
|
}
|
|
|
|
.state-card {
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
}
|
|
|
|
.state-card .label {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.state-card .value {
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
font-family: 'IBM Plex Mono', monospace;
|
|
}
|
|
|
|
.state-card .bar {
|
|
height: 4px;
|
|
background: var(--bg-tertiary);
|
|
border-radius: 2px;
|
|
margin-top: 8px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.state-card .bar-fill {
|
|
height: 100%;
|
|
border-radius: 2px;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.bar-thirst .bar-fill { background: var(--accent-blue); }
|
|
.bar-hunger .bar-fill { background: var(--accent-orange); }
|
|
.bar-heat .bar-fill { background: var(--accent-red); }
|
|
.bar-energy .bar-fill { background: var(--accent-green); }
|
|
|
|
.plan-visualization {
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.plan-visualization h3 {
|
|
font-size: 14px;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.plan-steps {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.plan-step {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.step-node {
|
|
padding: 12px 20px;
|
|
background: var(--bg-tertiary);
|
|
border: 2px solid var(--border-color);
|
|
border-radius: 8px;
|
|
font-family: 'IBM Plex Mono', monospace;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.step-node.current {
|
|
border-color: var(--accent-green);
|
|
background: rgba(63, 185, 80, 0.15);
|
|
color: var(--accent-green);
|
|
}
|
|
|
|
.step-arrow {
|
|
color: var(--text-muted);
|
|
font-size: 20px;
|
|
}
|
|
|
|
.goal-result {
|
|
padding: 12px 20px;
|
|
background: rgba(163, 113, 247, 0.15);
|
|
border: 2px solid var(--accent-purple);
|
|
border-radius: 8px;
|
|
color: var(--accent-purple);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.no-plan {
|
|
text-align: center;
|
|
padding: 40px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.goals-chart-container {
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
}
|
|
|
|
.goals-chart-container h3 {
|
|
font-size: 14px;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.chart-wrapper {
|
|
height: 300px;
|
|
}
|
|
|
|
.detail-section {
|
|
padding: 16px;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
|
|
.detail-section h3 {
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.detail-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 8px;
|
|
}
|
|
|
|
.detail-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: 6px 0;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.detail-item .label {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.detail-item .value {
|
|
font-family: 'IBM Plex Mono', monospace;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.action-list {
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.action-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 8px 12px;
|
|
border-radius: 6px;
|
|
margin-bottom: 4px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.action-item.valid {
|
|
background: var(--bg-tertiary);
|
|
}
|
|
|
|
.action-item.invalid {
|
|
background: transparent;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.action-item.in-plan {
|
|
background: rgba(63, 185, 80, 0.15);
|
|
border: 1px solid var(--accent-green);
|
|
}
|
|
|
|
.action-item .action-name {
|
|
flex: 1;
|
|
font-family: 'IBM Plex Mono', monospace;
|
|
}
|
|
|
|
.action-item .action-cost {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
margin-left: 8px;
|
|
}
|
|
|
|
.action-item .action-order {
|
|
width: 20px;
|
|
height: 20px;
|
|
background: var(--accent-green);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: #000;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.inventory-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 8px;
|
|
}
|
|
|
|
.inv-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px;
|
|
background: var(--bg-tertiary);
|
|
border-radius: 6px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.inv-item .icon {
|
|
font-size: 16px;
|
|
}
|
|
|
|
.inv-item .count {
|
|
margin-left: auto;
|
|
font-family: 'IBM Plex Mono', monospace;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.loading {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100%;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.urgency-indicator {
|
|
display: inline-block;
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
margin-left: 8px;
|
|
}
|
|
|
|
.urgency-none { background: var(--accent-green); }
|
|
.urgency-low { background: var(--accent-orange); }
|
|
.urgency-high { background: var(--accent-red); }
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.5; }
|
|
}
|
|
|
|
.updating {
|
|
animation: pulse 1s infinite;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header class="header">
|
|
<h1>🧠 GOAP Debug Visualizer</h1>
|
|
<div class="header-controls">
|
|
<span id="turn-display">Turn 0</span>
|
|
<span id="status-badge" class="status-badge status-disconnected">Disconnected</span>
|
|
<button class="btn" onclick="refreshData()">↻ Refresh</button>
|
|
<button class="btn btn-primary" id="auto-refresh-btn" onclick="toggleAutoRefresh()">▶ Auto</button>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="main-content">
|
|
<!-- Left Panel: Agent List -->
|
|
<div class="panel">
|
|
<div class="panel-header">
|
|
<h2>Agents</h2>
|
|
</div>
|
|
<div id="agent-list" class="agent-list">
|
|
<div class="loading">Loading...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Center Panel: Plan Visualization -->
|
|
<div class="center-panel">
|
|
<div class="plan-view" id="plan-view">
|
|
<div class="loading">Select an agent to view GOAP details</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Panel: Details -->
|
|
<div class="panel">
|
|
<div class="panel-header">
|
|
<h2>Details</h2>
|
|
</div>
|
|
<div id="details-panel">
|
|
<div class="loading">Select an agent</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
const API_BASE = 'http://localhost:8000/api';
|
|
let selectedAgentId = null;
|
|
let allAgentsData = [];
|
|
let autoRefreshInterval = null;
|
|
let goalsChart = null;
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
refreshData();
|
|
});
|
|
|
|
async function refreshData() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/goap/debug`);
|
|
if (!response.ok) throw new Error('API error');
|
|
|
|
const data = await response.json();
|
|
allAgentsData = data.agents;
|
|
|
|
document.getElementById('turn-display').textContent = `Turn ${data.current_turn}`;
|
|
document.getElementById('status-badge').className = 'status-badge status-connected';
|
|
document.getElementById('status-badge').textContent = data.is_night ? '🌙 Night' : '☀️ Connected';
|
|
|
|
renderAgentList();
|
|
|
|
if (selectedAgentId) {
|
|
const agent = allAgentsData.find(a => a.agent_id === selectedAgentId);
|
|
if (agent) {
|
|
renderAgentDetails(agent);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch data:', error);
|
|
document.getElementById('status-badge').className = 'status-badge status-disconnected';
|
|
document.getElementById('status-badge').textContent = 'Disconnected';
|
|
}
|
|
}
|
|
|
|
function renderAgentList() {
|
|
const container = document.getElementById('agent-list');
|
|
|
|
if (allAgentsData.length === 0) {
|
|
container.innerHTML = '<div class="loading">No agents found</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = allAgentsData.map(agent => `
|
|
<div class="agent-item ${agent.agent_id === selectedAgentId ? 'selected' : ''}"
|
|
onclick="selectAgent('${agent.agent_id}')">
|
|
<div class="agent-name">${agent.agent_name}</div>
|
|
<div class="agent-action">${agent.selected_action || 'No action'}</div>
|
|
<div class="agent-goal">${agent.current_plan ? '🎯 ' + agent.current_plan.goal_name : '(reactive)'}</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function selectAgent(agentId) {
|
|
selectedAgentId = agentId;
|
|
renderAgentList();
|
|
|
|
const agent = allAgentsData.find(a => a.agent_id === agentId);
|
|
if (agent) {
|
|
renderAgentDetails(agent);
|
|
}
|
|
}
|
|
|
|
function renderAgentDetails(agent) {
|
|
renderPlanView(agent);
|
|
renderDetailsPanel(agent);
|
|
}
|
|
|
|
function getUrgencyClass(urgency) {
|
|
if (urgency <= 0) return 'urgency-none';
|
|
if (urgency <= 1) return 'urgency-low';
|
|
return 'urgency-high';
|
|
}
|
|
|
|
function renderPlanView(agent) {
|
|
const container = document.getElementById('plan-view');
|
|
const ws = agent.world_state;
|
|
const plan = agent.current_plan;
|
|
|
|
container.innerHTML = `
|
|
<div class="plan-header">
|
|
<h2>${agent.agent_name}</h2>
|
|
${plan ? `<span class="plan-goal-badge">🎯 ${plan.goal_name}</span>` : ''}
|
|
</div>
|
|
|
|
<div class="world-state-grid">
|
|
<div class="state-card bar-thirst">
|
|
<div class="label">Thirst</div>
|
|
<div class="value">${Math.round(ws.vitals.thirst * 100)}%
|
|
<span class="urgency-indicator ${getUrgencyClass(ws.urgencies.thirst)}"></span>
|
|
</div>
|
|
<div class="bar"><div class="bar-fill" style="width: ${ws.vitals.thirst * 100}%"></div></div>
|
|
</div>
|
|
<div class="state-card bar-hunger">
|
|
<div class="label">Hunger</div>
|
|
<div class="value">${Math.round(ws.vitals.hunger * 100)}%
|
|
<span class="urgency-indicator ${getUrgencyClass(ws.urgencies.hunger)}"></span>
|
|
</div>
|
|
<div class="bar"><div class="bar-fill" style="width: ${ws.vitals.hunger * 100}%"></div></div>
|
|
</div>
|
|
<div class="state-card bar-heat">
|
|
<div class="label">Heat</div>
|
|
<div class="value">${Math.round(ws.vitals.heat * 100)}%
|
|
<span class="urgency-indicator ${getUrgencyClass(ws.urgencies.heat)}"></span>
|
|
</div>
|
|
<div class="bar"><div class="bar-fill" style="width: ${ws.vitals.heat * 100}%"></div></div>
|
|
</div>
|
|
<div class="state-card bar-energy">
|
|
<div class="label">Energy</div>
|
|
<div class="value">${Math.round(ws.vitals.energy * 100)}%
|
|
<span class="urgency-indicator ${getUrgencyClass(ws.urgencies.energy)}"></span>
|
|
</div>
|
|
<div class="bar"><div class="bar-fill" style="width: ${ws.vitals.energy * 100}%"></div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="plan-visualization">
|
|
<h3>Current Plan</h3>
|
|
${plan && plan.actions.length > 0 ? `
|
|
<div class="plan-steps">
|
|
${plan.actions.map((action, i) => `
|
|
<div class="plan-step">
|
|
<div class="step-node ${i === 0 ? 'current' : ''}">${action}</div>
|
|
${i < plan.actions.length - 1 ? '<span class="step-arrow">→</span>' : ''}
|
|
</div>
|
|
`).join('')}
|
|
<span class="step-arrow">→</span>
|
|
<div class="goal-result">✓ ${plan.goal_name}</div>
|
|
</div>
|
|
<div style="margin-top: 12px; font-size: 13px; color: var(--text-muted);">
|
|
Total Cost: ${plan.total_cost.toFixed(1)} | Steps: ${plan.plan_length}
|
|
</div>
|
|
` : `
|
|
<div class="no-plan">
|
|
<p style="font-size: 16px; margin-bottom: 8px;">No plan - using reactive selection</p>
|
|
<p>Selected: <strong>${agent.selected_action || 'None'}</strong></p>
|
|
</div>
|
|
`}
|
|
</div>
|
|
|
|
<div class="goals-chart-container">
|
|
<h3>Goal Priorities</h3>
|
|
<div class="chart-wrapper">
|
|
<canvas id="goals-chart"></canvas>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
renderGoalsChart(agent);
|
|
}
|
|
|
|
function renderGoalsChart(agent) {
|
|
const ctx = document.getElementById('goals-chart');
|
|
if (!ctx) return;
|
|
|
|
// Sort goals by priority
|
|
const sortedGoals = [...agent.goals].sort((a, b) => b.priority - a.priority);
|
|
const topGoals = sortedGoals.slice(0, 10);
|
|
|
|
if (goalsChart) {
|
|
goalsChart.destroy();
|
|
}
|
|
|
|
goalsChart = new Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: topGoals.map(g => g.name),
|
|
datasets: [{
|
|
label: 'Priority',
|
|
data: topGoals.map(g => g.priority),
|
|
backgroundColor: topGoals.map(g => {
|
|
if (g.is_selected) return 'rgba(163, 113, 247, 0.8)';
|
|
if (g.is_satisfied) return 'rgba(63, 185, 80, 0.5)';
|
|
if (g.priority > 0) return 'rgba(88, 166, 255, 0.7)';
|
|
return 'rgba(110, 118, 129, 0.3)';
|
|
}),
|
|
borderColor: topGoals.map(g => {
|
|
if (g.is_selected) return '#a371f7';
|
|
if (g.is_satisfied) return '#3fb950';
|
|
if (g.priority > 0) return '#58a6ff';
|
|
return '#6e7681';
|
|
}),
|
|
borderWidth: 2,
|
|
}]
|
|
},
|
|
options: {
|
|
indexAxis: 'y',
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { display: false },
|
|
},
|
|
scales: {
|
|
x: {
|
|
beginAtZero: true,
|
|
grid: { color: '#30363d' },
|
|
ticks: { color: '#8b949e' },
|
|
},
|
|
y: {
|
|
grid: { display: false },
|
|
ticks: {
|
|
color: '#e6edf3',
|
|
font: { family: 'IBM Plex Mono', size: 11 }
|
|
},
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderDetailsPanel(agent) {
|
|
const container = document.getElementById('details-panel');
|
|
const ws = agent.world_state;
|
|
|
|
const validActions = agent.actions.filter(a => a.is_valid);
|
|
const inPlanActions = agent.actions.filter(a => a.is_in_plan).sort((a, b) => a.plan_order - b.plan_order);
|
|
|
|
container.innerHTML = `
|
|
<div class="detail-section">
|
|
<h3>Inventory</h3>
|
|
<div class="inventory-grid">
|
|
<div class="inv-item"><span class="icon">💧</span> Water <span class="count">${ws.inventory.water}</span></div>
|
|
<div class="inv-item"><span class="icon">🍖</span> Meat <span class="count">${ws.inventory.meat}</span></div>
|
|
<div class="inv-item"><span class="icon">🫐</span> Berries <span class="count">${ws.inventory.berries}</span></div>
|
|
<div class="inv-item"><span class="icon">🪵</span> Wood <span class="count">${ws.inventory.wood}</span></div>
|
|
<div class="inv-item"><span class="icon">🥩</span> Hide <span class="count">${ws.inventory.hide}</span></div>
|
|
<div class="inv-item"><span class="icon">📦</span> Space <span class="count">${ws.inventory.space}</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="detail-section">
|
|
<h3>Economy</h3>
|
|
<div class="detail-grid">
|
|
<div class="detail-item">
|
|
<span class="label">Money</span>
|
|
<span class="value" style="color: var(--accent-orange)">${ws.economy.money}c</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="label">Wealthy</span>
|
|
<span class="value">${ws.economy.is_wealthy ? '✓' : '✗'}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="detail-section">
|
|
<h3>Market Access</h3>
|
|
<div class="detail-grid">
|
|
<div class="detail-item">
|
|
<span class="label">Buy Water</span>
|
|
<span class="value">${ws.market.can_buy_water ? '✓' : '✗'}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="label">Buy Food</span>
|
|
<span class="value">${ws.market.can_buy_food ? '✓' : '✗'}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="label">Buy Wood</span>
|
|
<span class="value">${ws.market.can_buy_wood ? '✓' : '✗'}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="detail-section">
|
|
<h3>Actions (${validActions.length} valid)</h3>
|
|
<div class="action-list">
|
|
${agent.actions.map(action => `
|
|
<div class="action-item ${action.is_valid ? 'valid' : 'invalid'} ${action.is_in_plan ? 'in-plan' : ''}">
|
|
${action.is_in_plan ? `<span class="action-order">${action.plan_order + 1}</span>` : ''}
|
|
<span class="action-name">${action.name}</span>
|
|
<span class="action-cost">${action.cost >= 0 ? action.cost.toFixed(1) : '∞'}</span>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function toggleAutoRefresh() {
|
|
const btn = document.getElementById('auto-refresh-btn');
|
|
|
|
if (autoRefreshInterval) {
|
|
clearInterval(autoRefreshInterval);
|
|
autoRefreshInterval = null;
|
|
btn.textContent = '▶ Auto';
|
|
btn.classList.remove('btn-primary');
|
|
} else {
|
|
autoRefreshInterval = setInterval(refreshData, 500);
|
|
btn.textContent = '⏸ Stop';
|
|
btn.classList.add('btn-primary');
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|