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:
+138
-11
@@ -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 {
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="nl">
|
||||
<script>(function(){try{var t=localStorage.getItem('flk-theme');if(t)document.documentElement.setAttribute('data-theme',t);}catch(e){}})();</script>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>War Room · Foodlinkk</title>
|
||||
<link rel="stylesheet" href="/static/css/palantir-theme.css?v=2" />
|
||||
<link rel="stylesheet" href="/static/css/flk-themes.css?v=1" />
|
||||
<link rel="stylesheet" href="/static/css/pulse-theme.css?v=2" />
|
||||
<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 defer src="/static/js/agents-warroom.js?v=18"></script>
|
||||
<script defer src="/static/js/alpine.min.js?v=2"></script>
|
||||
</head>
|
||||
<body class="wr-popup-body">
|
||||
@@ -22,7 +25,9 @@
|
||||
<template x-for="s in souls" :key="'p-'+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)">
|
||||
@click="selectAgent(s.agent_key)"
|
||||
@dblclick.prevent="openDm(s.agent_key)"
|
||||
title="Klik = @tag · dubbelklik = DM">
|
||||
<div class="agent-char-wrap"
|
||||
:data-agent-avatar="s.agent_key"
|
||||
:data-agent-color="s.avatar_color || '#1e3a5f'"
|
||||
@@ -41,12 +46,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>
|
||||
<a class="btn btn-sm" href="/agents?tab=warroom" target="_blank">In cockpit</a>
|
||||
<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.</p>
|
||||
<p class="wr-empty" x-show="!messages.length && !typing">Nog geen berichten.</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'}">
|
||||
@@ -64,22 +71,144 @@
|
||||
</template>
|
||||
</div>
|
||||
<div class="wr-bubble-body">
|
||||
<div class="wr-bubble-meta"><span x-text="labelForMessage(m)"></span></div>
|
||||
<div class="wr-bubble-meta">
|
||||
<span x-text="labelForMessage(m)"></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+'-pa-'+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… @agent" :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)">×</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="'pm-'+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… antwoord komt in deze 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>
|
||||
|
||||
<script src="/static/js/cockpit.js?v=3"></script>
|
||||
<script src="/static/js/agent-avatars.js?v=4"></script>
|
||||
<script src="/static/js/agents-warroom.js?v=4"></script>
|
||||
<!-- agents-warroom loaded in head (before alpine) -->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="nl">
|
||||
<script>(function(){try{var t=localStorage.getItem('flk-theme');if(t)document.documentElement.setAttribute('data-theme',t);var m=document.querySelector('meta[name=theme-color]');}catch(e){}})();</script>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, maximum-scale=5" />
|
||||
@@ -21,13 +22,13 @@
|
||||
<link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png" sizes="180x180" />
|
||||
<link rel="apple-touch-icon" href="/static/icons/icon-192.png" sizes="192x192" />
|
||||
<link rel="stylesheet" href="/static/css/palantir-theme.css?v=4" />
|
||||
<link rel="stylesheet" href="/static/css/flk-themes.css?v=1" />
|
||||
<link rel="stylesheet" href="/static/css/pulse-theme.css?v=2" />
|
||||
<link rel="stylesheet" href="/static/css/topnav-neo.css?v=6" />
|
||||
<link rel="stylesheet" href="/static/css/vertical-tabs.css?v=9" />
|
||||
<link rel="stylesheet" href="/static/css/herman-dashboard.css?v=34" />
|
||||
<script defer src="/static/js/herman-assistant.js?v=17"></script>
|
||||
<script defer src="/static/js/agents-warroom.js?v=4"></script>
|
||||
<script defer src="/static/js/alpine.min.js?v=2"></script>
|
||||
<script defer src="/static/js/alpine.min.js?v=2"></script>
|
||||
<script defer src="/static/js/htmx.min.js?v=2"></script>
|
||||
{% block extra_head %}{% endblock %}
|
||||
<link rel="stylesheet" href="/static/css/app-sidebar-neo.css?v=12" />
|
||||
@@ -528,6 +529,30 @@
|
||||
}, 2500);
|
||||
});
|
||||
</script>
|
||||
{% block scripts %}{% endblock %}
|
||||
<script>
|
||||
window.FlkThemes = {
|
||||
list: [
|
||||
{ id: 'ocean', name: 'Ocean Cyan', desc: 'Diep navy met fel cyaan' },
|
||||
{ id: 'aurora', name: 'Aurora', desc: 'Blauw-violet met cyaan highlights' },
|
||||
{ id: 'neon', name: 'Neon Tide', desc: 'Donker neon cyaan + magenta' },
|
||||
{ id: 'glacier', name: 'Glacier', desc: 'IJsblauw en helder sky-cyan' },
|
||||
{ id: 'cobalt', name: 'Cobalt Pulse', desc: 'Kobaltblauw met aqua en amber' }
|
||||
],
|
||||
current: function () {
|
||||
try { return localStorage.getItem('flk-theme') || 'ocean'; } catch (e) { return 'ocean'; }
|
||||
},
|
||||
apply: function (id) {
|
||||
var ok = this.list.some(function (t) { return t.id === id; });
|
||||
if (!ok) id = 'ocean';
|
||||
document.documentElement.setAttribute('data-theme', id);
|
||||
try { localStorage.setItem('flk-theme', id); } catch (e) {}
|
||||
return id;
|
||||
}
|
||||
};
|
||||
if (!document.documentElement.getAttribute('data-theme')) {
|
||||
window.FlkThemes.apply(window.FlkThemes.current());
|
||||
}
|
||||
</script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
{% if can_manage_users %}
|
||||
<a href="/settings?tab=users" class="settings-tab {% if active_tab == 'users' %}active{% endif %}">Gebruikers</a>
|
||||
{% endif %}
|
||||
<a href="/settings?tab=themes" class="settings-tab {% if active_tab == 'themes' %}active{% endif %}">Themes</a>
|
||||
<a href="/settings?tab=general" class="settings-tab {% if active_tab == 'general' %}active{% endif %}">General</a>
|
||||
</nav>
|
||||
|
||||
@@ -282,6 +283,26 @@
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% elif active_tab == 'themes' %}
|
||||
<section class="panel settings-panel" x-data="flkThemesPage()" x-init="init()">
|
||||
<div class="settings-panel-head">
|
||||
<div>
|
||||
<h2>UI Themes</h2>
|
||||
<p class="page-tip">Kies een kleurrijk cyaan/blauw thema voor de hele cockpit. Direct toegepast op alle pagina's.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flk-theme-grid">
|
||||
<template x-for="t in themes" :key="t.id">
|
||||
<button type="button" class="flk-theme-card" :class="{ 'is-active': current === t.id }" @click="select(t.id)">
|
||||
<div class="flk-theme-swatch" :style="swatch(t.id)"></div>
|
||||
<strong x-text="t.name"></strong>
|
||||
<small x-text="t.desc"></small>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% elif active_tab == 'general' %}
|
||||
<section class="panel settings-panel">
|
||||
<h2>General</h2>
|
||||
@@ -293,6 +314,33 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function flkThemesPage() {
|
||||
return {
|
||||
themes: (window.FlkThemes && window.FlkThemes.list) || [],
|
||||
current: 'ocean',
|
||||
init() {
|
||||
this.current = (window.FlkThemes && window.FlkThemes.current()) || 'ocean';
|
||||
if (window.FlkThemes) window.FlkThemes.apply(this.current);
|
||||
},
|
||||
select(id) {
|
||||
this.current = window.FlkThemes.apply(id);
|
||||
if (window.Cockpit) Cockpit.toast('Theme: ' + id, 'ok');
|
||||
},
|
||||
swatch(id) {
|
||||
var map = {
|
||||
ocean: 'linear-gradient(135deg,#07131f,#123049 40%,#22d3ee)',
|
||||
aurora: 'linear-gradient(135deg,#0a1020,#1a2550 45%,#a78bfa 75%,#22d3ee)',
|
||||
neon: 'linear-gradient(135deg,#050b14,#10243f 40%,#06b6d4 70%,#e879f9)',
|
||||
glacier: 'linear-gradient(135deg,#0b1522,#1a2f4a 45%,#38bdf8 80%,#7dd3fc)',
|
||||
cobalt: 'linear-gradient(135deg,#08101c,#172642 40%,#3b82f6 70%,#22d3ee)'
|
||||
};
|
||||
return 'background:' + (map[id] || map.ocean);
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function socialSettingsPage() {
|
||||
const META = {
|
||||
|
||||
Reference in New Issue
Block a user