Files

217 lines
13 KiB
HTML
Raw Permalink Normal View History

<!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=31" />
<script defer src="/static/js/agents-warroom.js?v=31"></script>
<script defer src="/static/js/alpine.min.js?v=2"></script>
</head>
<body class="wr-popup-body">
<div class="wr-popup-shell" x-data="warRoomPopup()" x-init="init()">
<div class="wr-layout">
<aside class="wr-roster">
<h3>Kanalen</h3>
<button type="button" class="wr-room-btn" :class="{active: room && room.kind==='warroom'}" @click="openWarroom()">
<span>⚔️</span><strong>#war-room</strong>
</button>
<h3 style="margin-top:0.85rem">Agents</h3>
<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="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'"
x-init="$nextTick(() => window.AgentAvatars && AgentAvatars.paint($el, s.agent_key, s.avatar_color))"></div>
<div class="wr-agent-meta">
<strong x-text="s.display_name"></strong>
<small x-text="'@'+s.agent_key"></small>
</div>
</button>
</template>
</aside>
<div class="wr-main">
<div class="wr-head">
<div>
<h2 x-text="room ? (room.title || 'War Room') : 'War Room'"></h2>
<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()" 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" @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 && !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'}">
<div class="wr-avatar" x-show="m.sender_type!=='system'">
<template x-if="m.sender_type==='agent'">
<div class="agent-char-wrap"
:data-agent-avatar="m.agent_key"
x-init="$nextTick(() => window.AgentAvatars && AgentAvatars.paint($el, m.agent_key))"></div>
</template>
<template x-if="m.sender_type==='user' && (m.sender_avatar_url || me.avatar_url)">
<img :src="m.sender_avatar_url || me.avatar_url" alt="" />
</template>
<template x-if="m.sender_type==='user' && !(m.sender_avatar_url || me.avatar_url)">
<span class="wr-avatar-fallback" x-text="(m.username||me.username||'?').slice(0,2).toUpperCase()"></span>
</template>
</div>
<div class="wr-bubble-body">
<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" @click="focusComposer()">
<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)" @keydown.enter.prevent="onEnter($event)"
placeholder="Typ hier… bv. @herman"
></textarea>
</div>
<button type="button" class="btn btn-pulse-green" @click="send()" :disabled="!canSend() || _sendLock">Stuur</button>
</div>
<template x-if="voiceRoomOpen">
<div class="wr-voice-overlay is-open" @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>
</template>
</div>
</div>
</div>
<script src="/static/js/cockpit.js?v=3"></script>
<script src="/static/js/agent-avatars.js?v=4"></script>
<!-- agents-warroom loaded in head (before alpine) -->
</body>
</html>