Ship Herman chat, Hermes KPI explorer, and intelligence analytics.
Adds floating Herman assistant with voice, Hermes technical KPI dashboard with file explorer, and a wins/kansen analytics pulse with Hermes findings hooks. Includes optional local HTTPS via Caddy for mic permissions (public certs only).
This commit is contained in:
+51
-19
@@ -21,12 +21,13 @@
|
||||
<link rel="stylesheet" href="/static/css/vertical-tabs.css?v=9" />
|
||||
<link rel="stylesheet" href="/static/css/mobile.css?v=5" />
|
||||
<link rel="stylesheet" href="/static/css/herman-dashboard.css?v=33" />
|
||||
<script defer src="/static/js/herman-assistant.js?v=16"></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=11" />
|
||||
<link rel="stylesheet" href="/static/css/cockpit-typography.css?v=1" />
|
||||
<link rel="stylesheet" href="/static/css/herman-assistant.css?v=1" />
|
||||
<link rel="stylesheet" href="/static/css/herman-assistant.css?v=16" />
|
||||
</head>
|
||||
<body x-data="appShell()" x-init="initApp()" :class="{ 'nav-open': navOpen, 'drawer-open': drawerOpen, 'brand-cucina': brand === 'cucina', 'brand-foodlinkk': brand === 'foodlinkk' }">
|
||||
<div class="app-shell">
|
||||
@@ -55,7 +56,6 @@
|
||||
<a href="/deals" class="nav-pill nav-pill-deals {% if request.url.path.startswith('/deals') %}active{% endif %}"><span class="sidebar-nav-icon">🤝</span><span class="sidebar-nav-label" data-i18n="nav.deals">Deals</span></a>
|
||||
<a href="/products" class="nav-pill nav-pill-products {% if request.url.path.startswith('/products') %}active{% endif %}"><span class="sidebar-nav-icon">📦</span><span class="sidebar-nav-label" data-i18n="nav.products">Producten</span></a>
|
||||
<a href="/suppliers" class="nav-pill nav-pill-suppliers {% if request.url.path.startswith('/suppliers') %}active{% endif %}"><span class="sidebar-nav-icon">🏭</span><span class="sidebar-nav-label" data-i18n="nav.suppliers">Leveranciers</span></a>
|
||||
<a href="/herman" class="nav-pill nav-pill-chat {% if request.url.path.startswith('/herman') %}active{% endif %}"><span class="sidebar-nav-icon">💬</span><span class="sidebar-nav-label" data-i18n="nav.chat">Herman</span></a>
|
||||
<a href="/reports" class="nav-pill nav-pill-reports {% if request.url.path.startswith('/reports') %}active{% endif %}"><span class="sidebar-nav-icon">📊</span><span class="sidebar-nav-label" data-i18n="nav.reports">Rapporten</span></a>
|
||||
</div>
|
||||
|
||||
@@ -111,14 +111,32 @@
|
||||
<button type="button" class="btn btn-sm" @click="showInstall=false">Later</button>
|
||||
</div>
|
||||
|
||||
<!-- Herman browser assistant (elke pagina) -->
|
||||
<div x-data="hermanBrowserWidget()" x-init="initWidget()" x-show="!hidden" x-cloak>
|
||||
<button type="button" class="ha-fab" :class="{ 'is-open': open }" @click="toggleOpen()" title="Praat met Herman" aria-label="Herman assistant">💬</button>
|
||||
<div class="ha-panel" x-show="open" x-transition @click.outside="open = false">
|
||||
<!-- Herman chat — altijd vast onderin zichtbaar -->
|
||||
<div id="ha-dock" class="ha-dock" x-data="hermanBrowserWidget()" x-init="initWidget()" :class="{ 'ha-dock-hidden': hidden }">
|
||||
<button type="button" class="ha-fab"
|
||||
:class="{ 'is-open': open, 'is-busy': busy, 'is-dragging': fabDragging, 'has-custom-pos': !!fabPos }"
|
||||
:style="fabStyle()"
|
||||
@pointerdown.prevent="onFabPointerDown($event)"
|
||||
@pointermove="onFabPointerMove($event)"
|
||||
@pointerup="onFabPointerUp($event)"
|
||||
@pointercancel="onFabPointerUp($event)"
|
||||
@click="onFabClick($event)"
|
||||
@dblclick="onFabDblClick($event)"
|
||||
title="Slepen om te verplaatsen · klik om te openen · dubbelklik = reset"
|
||||
aria-label="Open Herman chat">
|
||||
<span class="ha-fab-icon" aria-hidden="true">💬</span>
|
||||
<span class="ha-fab-label" x-text="open ? 'Sluiten' : 'Herman'"></span>
|
||||
</button>
|
||||
<div class="ha-panel" :class="{ 'has-custom-pos': !!fabPos }" :style="panelStyle()" x-show="open" x-cloak x-transition.opacity.duration.150ms @keydown.escape.window="open = false">
|
||||
<header class="ha-head">
|
||||
<h2>Herman <span style="font-size:0.65rem;color:#a78bfa">browser</span></h2>
|
||||
<div class="ha-head-title">
|
||||
<h2>Herman</h2>
|
||||
<p class="ha-conn" :class="{ 'is-ok': connOk, 'is-err': connOk === false }" x-text="connLabel"></p>
|
||||
</div>
|
||||
<div class="ha-head-actions">
|
||||
<a href="/voice">Voice live</a>
|
||||
<a x-show="!micSecureOk()" class="ha-text-btn" :href="httpsChatUrl()" target="_blank" rel="noopener" title="Audio/microfoon in HTTPS-tab">Audio</a>
|
||||
<button type="button" class="ha-text-btn" @click="clearChat()" title="Nieuwe chat" :disabled="busy || (!turns.length && !manualText)">Wissen</button>
|
||||
<a href="/herman">Volledig</a>
|
||||
<button type="button" @click="open = false" aria-label="Sluiten">×</button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -132,23 +150,39 @@
|
||||
<div class="ha-log" x-ref="chatLog">
|
||||
<template x-for="(t, i) in turns" :key="i">
|
||||
<div class="ha-bubble" :class="t.role === 'user' ? 'ha-bubble-user' : 'ha-bubble-agent'">
|
||||
<div class="ha-bubble-meta" x-text="t.at + (t.role === 'user' ? ' · jij' : ' · Herman')"></div>
|
||||
<div class="ha-bubble-meta" x-text="t.at + (t.role === 'user' ? ' · jij' : (' · ' + ((t.meta && t.meta.agent_label) || 'Herman')))"></div>
|
||||
<p x-text="t.text"></p>
|
||||
</div>
|
||||
</template>
|
||||
<p x-show="!turns.length" class="page-tip" style="font-size:0.75rem;margin:0">Typ of spreek — bv. “zoek distributeurs in Dubai”</p>
|
||||
<p x-show="!turns.length" class="page-tip" style="font-size:0.75rem;margin:0">Chat met Herman op de achtergrond — bv. “zoek distributeurs in Dubai”</p>
|
||||
</div>
|
||||
<div class="ha-preview" :class="{ 'is-interim': liveInterim }" x-show="livePreview || recording" x-text="livePreview || 'Luisteren…'"></div>
|
||||
<p class="ha-status" x-show="statusDetail" x-text="statusDetail"></p>
|
||||
<div class="ha-input-row">
|
||||
<div class="ha-toggles">
|
||||
<label title="Alleen voorlezen als jij dit aanzet">
|
||||
<input type="checkbox" x-model="speakReply" @change="persistPrefs()" />
|
||||
<span>Voorlezen (Maarten)</span>
|
||||
</label>
|
||||
<label title="Na antwoord meteen weer luisteren">
|
||||
<input type="checkbox" x-model="handsFreeMode" @change="onHandsFreeChange()" />
|
||||
<span>Hands-free praten</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="ha-status" :class="{ 'is-error': status === 'error' }" x-show="statusDetail || busy" x-text="statusDetail || (busy ? 'Herman denkt…' : '')"></p>
|
||||
<div class="ha-https-prompt" x-show="httpsAudioPrompt" x-cloak>
|
||||
<span>Microfoon werkt via HTTPS</span>
|
||||
<button type="button" class="btn btn-sm btn-pulse" @click="goHttpsAudio()">Doorgaan naar HTTPS →</button>
|
||||
<button type="button" class="ha-text-btn" @click="httpsAudioPrompt = false" title="Sluiten">×</button>
|
||||
</div>
|
||||
<form class="ha-input-row" @submit.prevent="sendText()">
|
||||
<button type="button" class="ha-mic" :class="micClass()"
|
||||
@mousedown.prevent="onMicDown()" @mouseup.prevent="onMicUp()"
|
||||
@touchstart.prevent="onMicDown()" @touchend.prevent="onMicUp()"
|
||||
@click.prevent="onMicClick()" title="Microfoon">🎙️</button>
|
||||
<input class="form-input" type="text" placeholder="Vraag aan Herman…" x-model="manualText"
|
||||
<input class="form-input" type="text" name="herman_message" autocomplete="off"
|
||||
placeholder="Vraag aan Herman…" x-model="manualText"
|
||||
@keydown.enter.prevent="sendText()" :disabled="busy" />
|
||||
<button type="button" class="btn btn-sm btn-pulse" @click="sendText()" :disabled="busy || !manualText.trim()">→</button>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-sm btn-pulse" :disabled="busy || !manualText.trim()">→</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="ei-modal-root ha-results-modal" x-show="resultsOpen" @click.self="closeResultsModal()">
|
||||
<div class="ei-modal-card ha-results-card" @click.stop>
|
||||
@@ -189,8 +223,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script defer src="/static/js/herman-assistant.js?v=2"></script>
|
||||
|
||||
<script src="/static/js/i18n.js?v=5"></script>
|
||||
<script src="/static/js/cockpit.js?v=2"></script>
|
||||
<script src="/static/js/viz-engine.js?v=3"></script>
|
||||
@@ -232,7 +264,7 @@
|
||||
_offset: 0,
|
||||
async initApp() {
|
||||
this.brand = window.FoodlinkkBrand.detect(location.pathname);
|
||||
if (!localStorage.getItem('flk-cache-v3')) {
|
||||
if (!localStorage.getItem('flk-cache-v4')) {
|
||||
if ('serviceWorker' in navigator) {
|
||||
const regs = await navigator.serviceWorker.getRegistrations();
|
||||
await Promise.all(regs.map((r) => r.unregister()));
|
||||
@@ -241,7 +273,7 @@
|
||||
const keys = await caches.keys();
|
||||
await Promise.all(keys.map((k) => caches.delete(k)));
|
||||
}
|
||||
localStorage.setItem('flk-cache-v3', '1');
|
||||
localStorage.setItem('flk-cache-v4', '1');
|
||||
}
|
||||
if (window.I18n) await window.I18n.init();
|
||||
this.locale = window.I18n ? window.I18n.locale() : 'nl';
|
||||
|
||||
Reference in New Issue
Block a user