Add War Room voice meeting, fix chat overlay, and ship 5 cyan UI themes.

Voice Room lets humans talk with agents via Whisper/TTS; chat typing no longer blocked by a full-screen overlay, attachments/avatars are larger, and Settings exposes five vivid mid-dark cyan/blue themes for the whole cockpit.
This commit is contained in:
Aissa
2026-08-16 15:49:16 +00:00
parent 1dd607b060
commit 910ba465d7
16 changed files with 2162 additions and 139 deletions
+138 -11
View File
@@ -5,7 +5,7 @@
<link rel="stylesheet" href="/static/css/herman-dashboard.css?v=13" />
<link rel="stylesheet" href="/static/css/agents-mesh.css?v=8" />
<link rel="stylesheet" href="/static/css/agent-characters.css?v=4" />
<link rel="stylesheet" href="/static/css/agents-warroom.css?v=3" />
<link rel="stylesheet" href="/static/css/agents-warroom.css?v=18" />
<script src="/static/js/agent-avatars.js?v=4"></script>
{% endblock %}
{% block content %}
@@ -272,7 +272,8 @@
</div>
</section>
<section x-show="tab==='warroom'" x-cloak x-data="warRoomHub()" x-init="init()">
<section x-show="tab==='warroom'" x-cloak>
<div x-data="warRoomHub()" x-init="init()" class="wr-hub-root">
<div class="panel" style="padding:0.75rem">
<div class="wr-layout">
<aside class="wr-roster">
@@ -284,8 +285,9 @@
<template x-for="s in souls" :key="'wr-'+s.agent_key">
<button type="button" class="wr-agent-btn"
:class="{active: room && room.kind==='dm' && room.agent_key===s.agent_key}"
@click="openDm(s.agent_key)"
@dblclick="insertMention(s.agent_key)">
@click="selectAgent(s.agent_key)"
@dblclick.prevent="openDm(s.agent_key)"
title="Klik = @tag in War Room · dubbelklik = DM">
<div class="agent-char-wrap"
:data-agent-avatar="s.agent_key"
:data-agent-color="s.avatar_color || '#1e3a5f'"
@@ -304,12 +306,14 @@
<small class="muted" x-show="status" x-text="status"></small>
</div>
<div class="wr-head-actions">
<label class="btn btn-sm" style="display:inline-flex;align-items:center;gap:0.3rem;cursor:pointer;margin:0" title="Agents praten onderling door"><input type="checkbox" x-model="autoDebate" @change="autoDebate && scheduleContinue(1500)" style="margin:0" /> A2A</label>
<button type="button" class="btn btn-sm btn-pulse" @click="openVoiceRoom()" :disabled="!room" title="Spraakmeeting met agents">🎙️ Voice Room</button><button type="button" class="btn btn-sm" :class="{'is-recording': voiceRecording}" @mousedown.prevent="startVoiceTalk($event)" @mouseup.prevent="stopVoiceTalk($event)" @touchstart.prevent="startVoiceTalk($event)" @touchend.prevent="stopVoiceTalk($event)" :disabled="voiceBusy || busy || !room" title="Snel praten">🎤</button>
<button type="button" class="btn btn-sm" @click="popOut()" title="Open op tweede scherm">Pop-out ↗</button>
<button type="button" class="btn btn-sm btn-pulse" @click="room && openRoom(room.id)" :disabled="busy">↻</button>
<button type="button" class="btn btn-sm" @click="room && openRoom(room.id)" :disabled="busy">↻</button>
</div>
</div>
<div class="wr-log" x-ref="wrLog">
<p class="wr-empty" x-show="!messages.length">Nog geen berichten — tag agents met @marketing of open een DM.</p>
<p class="wr-empty" x-show="!messages.length && !typing">Nog geen berichten — tag agents met @marketing of deel een bestand.</p>
<template x-for="m in messages" :key="m.id">
<div class="wr-bubble"
:class="{'is-user': m.sender_type==='user', 'is-agent': m.sender_type==='agent', 'is-system': m.sender_type==='system'}">
@@ -331,21 +335,144 @@
<div class="wr-bubble-meta">
<span x-text="labelForMessage(m)"></span>
<span x-show="m.agent_key" x-text="' · @'+m.agent_key"></span>
<span x-show="m.metadata && m.metadata.in_reply_to" x-text="' · reageert op @'+m.metadata.in_reply_to"></span>
<button type="button" class="wr-msg-del" title="Verwijderen"
@click.stop="deleteMessage(m)">🗑</button>
</div>
<div class="wr-bubble-text" x-text="m.content"></div>
<div class="wr-attach-list" x-show="attachmentsOf(m).length">
<template x-for="(a, ai) in attachmentsOf(m)" :key="m.id+'-a-'+ai">
<a class="wr-attach-card" :href="a.url" target="_blank" rel="noopener">
<img x-show="isImage(a)" :src="a.url" :alt="a.name" />
<span x-text="a.name"></span>
</a>
</template>
</div>
</div>
</div>
</template>
<div class="wr-bubble is-agent is-typing" x-show="typing" x-cloak>
<div class="wr-avatar">
<div class="agent-char-wrap"
:data-agent-avatar="typing && typing.agent_key"
x-effect="if (typing && window.AgentAvatars) AgentAvatars.paint($el, typing.agent_key)"></div>
</div>
<div class="wr-bubble-body">
<div class="wr-bubble-meta">
<span x-text="(typing && typing.display_name) || 'Agent'"></span>
<span> · typt</span>
</div>
<div class="wr-bubble-text wr-typing-cursor" x-text="(typing && typing.text) || '…'"></div>
</div>
</div>
</div>
<div class="wr-composer">
<textarea class="form-input" rows="2" x-model="draft" @keydown="onKey($event)"
placeholder="Bericht… gebruik @agent om iemand te taggen" :disabled="busy || !room"></textarea>
<button type="button" class="btn btn-pulse-green" @click="send()" :disabled="busy || !room || !draft.trim()">Stuur</button>
<div class="wr-composer-tools">
<button type="button" class="btn btn-sm" :class="{'is-recording': voiceRecording}" @mousedown.prevent="startVoiceTalk($event)" @mouseup.prevent="stopVoiceTalk($event)" @touchstart.prevent="startVoiceTalk($event)" @touchend.prevent="stopVoiceTalk($event)" :disabled="voiceBusy || busy" title="Houd in om te praten">🎤</button>
<button type="button" class="btn btn-sm" @click="emojiOpen=!emojiOpen" :disabled="busy">😊</button>
<label class="btn btn-sm" style="cursor:pointer;margin:0">
📎
<input type="file" hidden multiple @change="onPickFiles($event)"
accept=".png,.jpg,.jpeg,.gif,.webp,.pdf,.txt,.csv,.xlsx,.xls,.doc,.docx,.zip,.json,.md,image/*" />
</label>
<div class="wr-emoji-pop" x-show="emojiOpen" @click.outside="emojiOpen=false" x-cloak>
<template x-for="em in emojis" :key="em">
<button type="button" @click="insertEmoji(em)" x-text="em"></button>
</template>
</div>
</div>
<div class="wr-pending" x-show="pendingFiles.length">
<template x-for="(f, fi) in pendingFiles" :key="f.url">
<span class="wr-pending-chip">
<span x-text="f.name"></span>
<button type="button" @click="removePending(fi)" title="Verwijderen">×</button>
</span>
</template>
</div>
<div class="wr-composer-wrap">
<div class="wr-mention-pop" x-show="mentionOpen && mentionSuggestions().length" x-cloak>
<template x-for="(s, si) in mentionSuggestions()" :key="'m-'+s.agent_key">
<button type="button" class="wr-mention-item"
:class="{active: si===mentionIndex}"
@mousedown.prevent="pickMention(s)">
<strong x-text="s.display_name"></strong>
<small x-text="'@'+s.agent_key"></small>
</button>
</template>
</div>
<textarea class="form-input" rows="2" x-ref="wrInput" x-model="draft"
@input="onDraftInput()" @keydown="onKey($event)"
placeholder="Typ @bizdev of @finance… agent antwoordt hier in de War Room"
:disabled="!room"></textarea>
</div>
<button type="button" class="btn btn-pulse-green" @click="send()" :disabled="!canSend()">Stuur</button>
</div>
<div class="wr-voice-overlay" :class="{ 'is-open': voiceRoomOpen }" x-show="voiceRoomOpen" x-cloak @keydown.escape.window="closeVoiceRoom()">
<div class="wr-voice-panel" @click.stop>
<div class="wr-voice-head">
<div>
<h3>🎙️ Foodlinkk Voice Room</h3>
<small class="muted">Jij + agents in één gesprek (spraak)</small>
</div>
<div class="wr-voice-head-actions">
<label class="btn btn-sm" style="display:inline-flex;align-items:center;gap:0.25rem;margin:0;cursor:pointer">
<input type="checkbox" x-model="speakReplies" style="margin:0" /> 🔊 Voorlezen
</label>
<button type="button" class="btn btn-sm btn-pulse" @click="closeVoiceRoom()">Sluiten</button>
</div>
</div>
<div class="wr-voice-status" x-text="status || (voiceRecording ? 'Luisteren…' : (voiceSpeakingKey ? (voiceSpeakingKey + ' spreekt…') : 'Houd de microfoon in om te praten'))"></div>
<div class="wr-voice-grid">
<div class="wr-voice-card is-human" :class="{'is-live': voiceRecording}">
<div class="wr-voice-avatar wr-voice-avatar-human" x-text="(me.username||'Jij').slice(0,2).toUpperCase()"></div>
<strong x-text="me.username || 'Jij'"></strong>
<small x-show="voiceRecording">🎤 aan het woord</small>
<small x-show="!voiceRecording" class="muted">mens</small>
</div>
<template x-for="s in voiceAgentSouls()" :key="'vr-'+s.agent_key">
<div class="wr-voice-card is-agent"
:class="{'is-live': voiceSpeakingKey===s.agent_key, 'is-off': !isVoiceAgent(s.agent_key)}"
@click="toggleVoiceAgent(s.agent_key)"
:title="isVoiceAgent(s.agent_key) ? 'Klik = uit meeting' : 'Klik = in meeting'">
<div class="wr-voice-avatar agent-char-wrap"
:data-agent-avatar="s.agent_key"
x-init="$nextTick(() => window.AgentAvatars && AgentAvatars.paint($el, s.agent_key))"></div>
<strong x-text="s.display_name || s.agent_key"></strong>
<small x-show="voiceSpeakingKey===s.agent_key">🔊 spreekt</small>
<small x-show="voiceSpeakingKey!==s.agent_key" class="muted" x-text="isVoiceAgent(s.agent_key) ? ('@'+s.agent_key) : 'niet in meeting'"></small>
</div>
</template>
</div>
<p class="wr-voice-hint" x-show="voiceLastHeard">Laatst gehoord: <span x-text="voiceLastHeard"></span></p>
<div class="wr-voice-ptt">
<button type="button" class="wr-ptt-btn"
:class="{'is-recording': voiceRecording, 'is-busy': voiceBusy}"
@mousedown.prevent="startVoiceTalk($event)" @mouseup.prevent="stopVoiceTalk($event)"
@touchstart.prevent="startVoiceTalk($event)" @touchend.prevent="stopVoiceTalk($event)"
@mouseleave="voiceRecording && stopVoiceTalk($event)"
:disabled="voiceBusy || busy">
<span x-show="!voiceRecording && !voiceBusy">🎤 Ingedrukt houden</span>
<span x-show="voiceRecording">● Luisteren… laat los</span>
<span x-show="voiceBusy && !voiceRecording">⏳ Bezig…</span>
</button>
</div>
<div class="wr-voice-foot">
<span class="muted">Klik agents om ze in/uit de meeting te zetten. Standaard praat @herman; 🔊 leest antwoorden voor.</span>
<a class="btn btn-sm" href="https://meet.ffmuc.net/FoodlinkkWR1" :href="callHref()" target="_blank" rel="noopener">Alleen-mensen video ↗</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
@@ -389,7 +516,7 @@
<script src="/static/js/page-viz.js?v=1"></script>
<script src="/static/js/agents-mesh.js?v=11"></script>
<script src="/static/js/agents-terminals.js?v=6"></script>
<script src="/static/js/agents-warroom.js?v=4"></script>
<script src="/static/js/agents-warroom.js?v=18"></script>
<script>
function agentsSoulHub() {
return {