SysOps: voice-agy-webbuilder-backup — 2026-06-23 10:04 UTC
This commit is contained in:
+211
-13
@@ -3,7 +3,7 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
<link rel="stylesheet" href="/static/css/hermes.css" />
|
||||
<link rel="stylesheet" href="/static/css/herman-dashboard.css?v=13" />
|
||||
<link rel="stylesheet" href="/static/css/agents-mesh.css" />
|
||||
<link rel="stylesheet" href="/static/css/agents-mesh.css?v=8" />
|
||||
<link rel="stylesheet" href="/static/css/agent-characters.css?v=3" />
|
||||
<script src="/static/js/agent-avatars.js?v=3"></script>
|
||||
{% endblock %}
|
||||
@@ -25,12 +25,16 @@
|
||||
|
||||
<div class="vtabs-layout" style="--vt-color:#ffd700" x-ref="vtabsWrap">
|
||||
<nav class="vtabs-nav" x-ref="vtabsNav" :style="'--vt-indicator-top:'+tabIndicatorTop+'px;--vt-indicator-h:'+tabIndicatorH+'px'">
|
||||
<div class="vtabs-nav-header"><strong>Agents</strong></div>
|
||||
<button class="vtab-btn" :class="{active: tab==='souls'}" @click="setTab('souls', $el)">
|
||||
<span class="vtab-icon">🧬</span><span>Souls</span>
|
||||
</button>
|
||||
<button class="vtab-btn" :class="{active: tab==='mesh'}" @click="setTab('mesh', $el)">
|
||||
<span class="vtab-icon">🕸️</span><span>Netwerk</span>
|
||||
</button>
|
||||
<button class="vtab-btn" :class="{active: tab==='terminals'}" @click="setTab('terminals', $el)" data-tab="terminals">
|
||||
<span class="vtab-icon">🖥️</span><span>Terminals</span>
|
||||
</button>
|
||||
<button class="vtab-btn" :class="{active: tab==='approvals'}" @click="setTab('approvals', $el)">
|
||||
<span class="vtab-icon">✓</span><span>Goedkeuringen</span>
|
||||
<span class="vtab-badge" x-show="pendingCount" x-text="pendingCount"></span>
|
||||
@@ -62,6 +66,9 @@
|
||||
<p class="agent-current-task" x-text="s.current_task || 'Wacht op taak…'"></p>
|
||||
<p class="agent-role-desc" x-text="s.responsibilities || '—'"></p>
|
||||
<div class="agent-meta-row">
|
||||
<button type="button" class="agent-events-link"
|
||||
@click.stop="goTerminal(s.agent_key)"
|
||||
title="Open live terminal">Terminal →</button>
|
||||
<button type="button" class="agent-events-link"
|
||||
@click.stop="openEvents(s.agent_key, s.display_name)"
|
||||
x-text="(s.event_count||0) + ' events'"
|
||||
@@ -105,18 +112,114 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section x-show="tab==='mesh'" x-cloak>
|
||||
<section x-show="tab==='mesh'" x-cloak
|
||||
x-effect="if (tab === 'mesh') { $nextTick(() => mountMesh()) }">
|
||||
<div class="panel">
|
||||
<h2 style="margin-top:0;">Netwerk</h2>
|
||||
<p class="page-tip" style="margin-top:.25rem;">Agents onderaan → Herman (Co-CEO) → CEO & CTO. Pulse = actieve communicatie.</p>
|
||||
<div class="mesh-framework-head">
|
||||
<h2 style="margin-top:0;">Agent Communicatie Framework</h2>
|
||||
<p class="page-tip" style="margin-top:.25rem;">Kies hoe je het netwerk wilt zien. Klik een agent → live terminal. Alle terminals onder tab <strong>Terminals</strong>.</p>
|
||||
</div>
|
||||
|
||||
<div class="mesh-mode-section" id="mesh-mode-picker">
|
||||
<label>Communicatiestijl</label>
|
||||
<div class="mesh-mode-grid">
|
||||
<button type="button" class="mesh-mode-card" data-mesh-mode="pyramid" aria-pressed="false"
|
||||
onclick="window.AgentsMesh&&AgentsMesh.pickMode('pyramid')">
|
||||
<span class="mesh-mode-check" aria-hidden="true"></span>
|
||||
<span class="mesh-mode-icon">◆</span>
|
||||
<span class="mesh-mode-title">Piramide</span>
|
||||
<span class="mesh-mode-sub">Herman als hub</span>
|
||||
<span class="mesh-mode-desc">Rapport ↑ en delegatie ↓. Alleen live handoffs.</span>
|
||||
</button>
|
||||
<button type="button" class="mesh-mode-card" data-mesh-mode="mesh" aria-pressed="false"
|
||||
onclick="window.AgentsMesh&&AgentsMesh.pickMode('mesh')">
|
||||
<span class="mesh-mode-check" aria-hidden="true"></span>
|
||||
<span class="mesh-mode-icon">⬡</span>
|
||||
<span class="mesh-mode-title">Vol netwerk</span>
|
||||
<span class="mesh-mode-sub">Alle partners</span>
|
||||
<span class="mesh-mode-desc">Volledige samenwerkingsmatrix zichtbaar.</span>
|
||||
</button>
|
||||
<button type="button" class="mesh-mode-card" data-mesh-mode="org" aria-pressed="false"
|
||||
onclick="window.AgentsMesh&&AgentsMesh.pickMode('org')">
|
||||
<span class="mesh-mode-check" aria-hidden="true"></span>
|
||||
<span class="mesh-mode-icon">▤</span>
|
||||
<span class="mesh-mode-title">Organigram</span>
|
||||
<span class="mesh-mode-sub">Pure hiërarchie</span>
|
||||
<span class="mesh-mode-desc">CEO → Herman → agents, geen peer-lijnen.</span>
|
||||
</button>
|
||||
<button type="button" class="mesh-mode-card" data-mesh-mode="pulse" aria-pressed="false"
|
||||
onclick="window.AgentsMesh&&AgentsMesh.pickMode('pulse')">
|
||||
<span class="mesh-mode-check" aria-hidden="true"></span>
|
||||
<span class="mesh-mode-icon">◎</span>
|
||||
<span class="mesh-mode-title">Live stream</span>
|
||||
<span class="mesh-mode-sub">Alleen actief</span>
|
||||
<span class="mesh-mode-desc">Geen stille lijnen — alleen wat nu beweegt.</span>
|
||||
</button>
|
||||
<button type="button" class="mesh-mode-card" data-mesh-mode="clusters" aria-pressed="false"
|
||||
onclick="window.AgentsMesh&&AgentsMesh.pickMode('clusters')">
|
||||
<span class="mesh-mode-check" aria-hidden="true"></span>
|
||||
<span class="mesh-mode-icon">◉</span>
|
||||
<span class="mesh-mode-title">Domeinen</span>
|
||||
<span class="mesh-mode-sub">Per afdeling</span>
|
||||
<span class="mesh-mode-desc">Groei · Product · Operatie gegroepeerd.</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mesh-toolbar">
|
||||
<span id="mesh-mode-badge" class="mesh-mode-badge">Piramide</span>
|
||||
<button type="button" class="btn btn-sm" @click="remountMesh()">↻ Verversen</button>
|
||||
<button type="button" class="btn btn-sm btn-pulse" @click="setTab('terminals', $refs.vtabsNav?.querySelector('[data-tab=terminals]'))">Alle terminals</button>
|
||||
</div>
|
||||
<div class="mesh-wrap">
|
||||
<svg id="agents-mesh-canvas" class="mesh-svg" viewBox="0 0 1100 640"></svg>
|
||||
<svg id="agents-mesh-canvas" class="mesh-svg" viewBox="0 0 1200 860"></svg>
|
||||
</div>
|
||||
<div class="mesh-legend">
|
||||
<span>Groen = healthy</span>
|
||||
<span>Oranje = waarschuwing</span>
|
||||
<span>Blauw = idle</span>
|
||||
<span>Grijs = offline</span>
|
||||
<span class="leg-report">↑ Rapport</span>
|
||||
<span class="leg-delegate">↓ Delegatie</span>
|
||||
<span class="leg-peer" x-show="meshLegendPeer">↔ Handoff</span>
|
||||
<span class="leg-exec" x-show="meshLegendExec">⇅ Executive</span>
|
||||
</div>
|
||||
<div class="mesh-terminal-focus" x-show="focusTerminalKey" x-cloak>
|
||||
<div class="agent-term-head">
|
||||
<span class="term-dot"></span>
|
||||
<strong x-text="'Terminal · ' + (focusTerminalName || '')"></strong>
|
||||
<button type="button" class="btn btn-sm" @click="clearFocusTerminal()">Sluiten</button>
|
||||
</div>
|
||||
<div class="agent-term-body" :data-term-key="focusTerminalKey" data-term-focus="1"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section x-show="tab==='terminals'" x-cloak>
|
||||
<div class="panel">
|
||||
<h2 style="margin-top:0;">Agent terminals</h2>
|
||||
<p class="agents-terminals-intro">Elke agent heeft een eigen terminal. Typ commando's en druk <kbd>Enter</kbd> — output verschijnt live. Start met <code>help</code>.</p>
|
||||
<div class="agents-terminals-grid">
|
||||
<template x-for="s in souls" :key="'term-' + s.agent_key">
|
||||
<article class="agent-term-window"
|
||||
:class="{ 'is-live': (s.events_6h||0) > 0 || s.current_task, 'is-expanded': expandedTerminal === s.agent_key }"
|
||||
@click="expandTerminal(s.agent_key)">
|
||||
<header class="agent-term-head">
|
||||
<span class="term-dot"></span>
|
||||
<strong x-text="s.display_name"></strong>
|
||||
<small x-text="s.current_task || s.last_event_title || 'Standby'"></small>
|
||||
</header>
|
||||
<div class="agent-term-body" :data-term-key="s.agent_key"></div>
|
||||
<footer class="agent-term-foot" @click.stop>
|
||||
<label class="agent-term-cmd" :for="'term-in-' + s.agent_key">
|
||||
<span class="term-prompt" x-text="s.agent_key + '>'"></span>
|
||||
<input type="text"
|
||||
:id="'term-in-' + s.agent_key"
|
||||
class="agent-term-input"
|
||||
:data-term-input="s.agent_key"
|
||||
:placeholder="s.agent_key === 'herman' ? 'briefing · vraag aan Herman…' : (s.agent_key === 'webbuilder' ? 'build <opdracht> · projects · status …' : 'help · status · handoff …')"
|
||||
autocomplete="off"
|
||||
spellcheck="false" />
|
||||
</label>
|
||||
</footer>
|
||||
</article>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -196,7 +299,8 @@
|
||||
{% block scripts %}
|
||||
<script src="/static/js/dashboard-layout.js?v=2"></script>
|
||||
<script src="/static/js/page-viz.js?v=1"></script>
|
||||
<script src="/static/js/agents-mesh.js"></script>
|
||||
<script src="/static/js/agents-mesh.js?v=11"></script>
|
||||
<script src="/static/js/agents-terminals.js?v=6"></script>
|
||||
<script>
|
||||
function agentsSoulHub() {
|
||||
return {
|
||||
@@ -207,6 +311,11 @@ function agentsSoulHub() {
|
||||
pendingCount: 0,
|
||||
souls: [], selected: null, form: {}, saving: false,
|
||||
eventsOpen: false, eventsAgentKey: null, eventsAgentName: '', eventsList: [], eventsLoading: false, selectedEvent: null,
|
||||
terminalOpen: false, terminalAgentKey: null, terminalAgentName: '', terminalLines: [], _terminalEs: null,
|
||||
meshCommMode: 'pyramid',
|
||||
meshLegendPeer: true,
|
||||
meshLegendExec: true,
|
||||
focusTerminalKey: null, focusTerminalName: '', expandedTerminal: null,
|
||||
get totalEvents() { return this.souls.reduce((a,s) => a + (s.event_count||0), 0); },
|
||||
get activeCount() { return this.souls.filter(s => s.is_active !== false).length; },
|
||||
statusClass(s) {
|
||||
@@ -260,6 +369,7 @@ function agentsSoulHub() {
|
||||
return 'Standby';
|
||||
},
|
||||
async init() {
|
||||
this.syncMeshMode();
|
||||
await this.load();
|
||||
await this.loadPendingCount();
|
||||
if (window.PageViz) {
|
||||
@@ -268,7 +378,17 @@ function agentsSoulHub() {
|
||||
this.$nextTick(() => {
|
||||
const activeBtn = this.$refs.vtabsNav?.querySelector('.vtab-btn.active');
|
||||
if (activeBtn) this.updateTabIndicator(activeBtn);
|
||||
const self = this;
|
||||
if (window.AgentsMesh) {
|
||||
AgentsMesh.bindModePicker({
|
||||
onNodeClick: function (node) {
|
||||
self.focusTerminal(node.agent_key, node.display_name);
|
||||
},
|
||||
});
|
||||
}
|
||||
if (window.AgentsTerminals) AgentsTerminals.startLiveStream();
|
||||
if (this.tab === 'mesh') this.mountMesh();
|
||||
if (this.tab === 'terminals') this.startTerminals();
|
||||
});
|
||||
},
|
||||
updateTabIndicator(el) {
|
||||
@@ -285,6 +405,7 @@ function agentsSoulHub() {
|
||||
url.searchParams.set('tab', next);
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
if (next === 'mesh') this.mountMesh();
|
||||
if (next === 'terminals') this.startTerminals();
|
||||
if (next === 'approvals') this.loadPendingCount();
|
||||
},
|
||||
async loadPendingCount() {
|
||||
@@ -293,10 +414,87 @@ function agentsSoulHub() {
|
||||
this.pendingCount = (r.items || []).length;
|
||||
} catch (e) { this.pendingCount = 0; }
|
||||
},
|
||||
syncMeshMode() {
|
||||
if (!window.AgentsMesh) return;
|
||||
const self = this;
|
||||
this.meshCommMode = AgentsMesh.getSavedMode();
|
||||
this.updateMeshLegend();
|
||||
AgentsMesh.setModeChangeCallback(function (id) {
|
||||
self.meshCommMode = id;
|
||||
self.updateMeshLegend();
|
||||
});
|
||||
},
|
||||
updateMeshLegend() {
|
||||
const m = (window.AgentsMesh && AgentsMesh.COMM_MODES) ? AgentsMesh.COMM_MODES[this.meshCommMode] : null;
|
||||
this.meshLegendPeer = m ? (m.showLivePeers || m.showStaticPeers) : true;
|
||||
this.meshLegendExec = m ? m.showExecutive !== false : true;
|
||||
},
|
||||
setMeshMode(id) {
|
||||
if (window.AgentsMesh) AgentsMesh.pickMode(id);
|
||||
this.meshCommMode = id;
|
||||
this.updateMeshLegend();
|
||||
},
|
||||
remountMesh() {
|
||||
if (window.AgentsMesh && AgentsMesh.refresh) {
|
||||
const self = this;
|
||||
AgentsMesh.refresh('agents-mesh-canvas', {
|
||||
commMode: this.meshCommMode,
|
||||
onNodeClick: function (node) {
|
||||
self.focusTerminal(node.agent_key, node.display_name);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.mountMesh();
|
||||
}
|
||||
},
|
||||
mountMesh() {
|
||||
if (this.meshLoaded || !window.AgentsMesh) return;
|
||||
this.meshLoaded = true;
|
||||
window.AgentsMesh.mount('agents-mesh-canvas');
|
||||
if (!window.AgentsMesh) return;
|
||||
const self = this;
|
||||
const opts = {
|
||||
commMode: this.meshCommMode,
|
||||
onNodeClick: function (node) {
|
||||
self.focusTerminal(node.agent_key, node.display_name);
|
||||
},
|
||||
};
|
||||
window.AgentsMesh.mount('agents-mesh-canvas', opts).catch(function (e) {
|
||||
if (window.Cockpit) Cockpit.toast('Mesh: ' + (e.message || 'laden mislukt'), 'error');
|
||||
});
|
||||
if (this.focusTerminalKey && window.AgentsTerminals) {
|
||||
this.$nextTick(function () { AgentsTerminals.mountAll(); });
|
||||
}
|
||||
},
|
||||
focusTerminal(key, name) {
|
||||
this.focusTerminalKey = key;
|
||||
this.focusTerminalName = name || key;
|
||||
this.expandedTerminal = key;
|
||||
this.$nextTick(function () {
|
||||
if (window.AgentsTerminals) AgentsTerminals.mountAll();
|
||||
});
|
||||
},
|
||||
clearFocusTerminal() {
|
||||
this.focusTerminalKey = null;
|
||||
this.focusTerminalName = '';
|
||||
},
|
||||
goTerminal(key) {
|
||||
const soul = this.souls.find(function (s) { return s.agent_key === key; });
|
||||
const btn = this.$refs.vtabsNav?.querySelector('[data-tab=terminals]');
|
||||
this.setTab('terminals', btn);
|
||||
this.expandedTerminal = key;
|
||||
this.$nextTick(function () {
|
||||
const el = document.querySelector('[data-term-key="' + key + '"]');
|
||||
if (el) el.closest('.agent-term-window')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
});
|
||||
},
|
||||
expandTerminal(key) {
|
||||
this.expandedTerminal = this.expandedTerminal === key ? null : key;
|
||||
},
|
||||
startTerminals() {
|
||||
this.$nextTick(function () {
|
||||
if (window.AgentsTerminals) {
|
||||
AgentsTerminals.mountAll();
|
||||
AgentsTerminals.bindInputs();
|
||||
}
|
||||
});
|
||||
},
|
||||
async load() {
|
||||
const r = await fetch('/api/agents/souls').then(x => x.json());
|
||||
|
||||
Reference in New Issue
Block a user