SysOps: voice-agy-webbuilder-backup — 2026-06-23 10:04 UTC

This commit is contained in:
sysops
2026-06-23 10:04:23 +00:00
parent 3bf15c4850
commit 26fe76afdd
165 changed files with 47427 additions and 1264 deletions
+103 -94
View File
@@ -1,103 +1,112 @@
{% extends "base.html" %}
{% block title %}Herman · Browser{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="/static/css/voice-live.css?v=2" />
{% endblock %}
{% block content %}
<div x-data="hermanChat()" class="chat-layout">
<header class="page-header">
<h1>Herman</h1>
<p class="subtitle page-tip">Co-CEO orchestrator — delegeert naar specialist agents (packaging, design, …).</p>
</header>
<div x-data="HermanAssistant.chatPage()" x-init="initChat()" class="chat-layout herman-browser-chat">
<header class="page-header">
<div>
<h1>Herman</h1>
<p class="subtitle page-tip">Praat of typ in de browser — voice, bevestiging, Export Intel popups.</p>
</div>
<div class="retail-actions">
<a class="btn btn-sm btn-pulse" href="/voice">Voice Live</a>
<a class="btn btn-sm" href="/export-intel">Wereldexport</a>
</div>
</header>
<div class="card card-compact" style="margin-bottom:1rem;">
<h2 style="font-size:1rem;margin:0 0 .5rem;">Verpakking via Packaging agent</h2>
<p class="page-tip" style="margin:0 0 .5rem;">Start met <code>maak verpakking:</code> — de agent genereert het design, slaat op op NAS en rapporteert aan Herman.</p>
<p class="page-tip" style="margin:0;font-size:0.78rem">Voorbeeld: <em>maak verpakking: pouch 100×140 voor Halal Kip Curry project #5 halal-badge</em></p>
</div>
<div class="card card-compact" style="margin-bottom:1rem;">
<h2 style="font-size:1rem;margin:0 0 .5rem;">Foto genereren (ComfyUI)</h2>
<p class="page-tip" style="margin:0 0 .75rem;">Beschrijf wat je wilt zien. Op CPU duurt dit 13 minuten.</p>
<form class="chat-input" @submit.prevent="generateImage">
<input class="form-input" x-model="imagePrompt" placeholder="bijv. halal kipnuggets productfoto, studio licht" autocomplete="off" />
<button class="btn btn-primary" type="submit" :disabled="generating">
<span x-show="!generating">Genereer</span>
<span x-show="generating">Bezig…</span>
</button>
</form>
<div x-show="generating" class="page-tip" style="margin-top:.5rem;">Ik ga even de gegevens ophalen — afbeelding wordt gemaakt…</div>
<div x-show="lastImage" style="margin-top:1rem;">
<img :src="lastImage" alt="Gegenereerde afbeelding" style="max-width:100%;border-radius:8px;border:1px solid var(--border);" />
<div class="voice-confirm-bar" x-show="pendingAction" x-cloak>
<div class="voice-confirm-text">
<strong>Bevestiging nodig</strong>
<span x-text="pendingAction ? ('Zoeken: ' + (pendingAction.entity_label || '') + ' in ' + (pendingAction.location_label || '')) : ''"></span>
</div>
<div class="voice-confirm-actions">
<button type="button" class="btn btn-sm btn-pulse" @click="confirmPending()" :disabled="busy">Ja, zoeken</button>
<button type="button" class="btn btn-sm" @click="cancelPending()" :disabled="busy">Annuleren</button>
</div>
</div>
</div>
<div class="chat-messages" id="chat-log">
{% if user_message %}<div class="chat-msg user"><p>{{ user_message }}</p></div>{% endif %}
{% if last_reply %}<div class="chat-msg agent"><span class="badge">{{ last_agent }}</span><p>{{ last_reply }}</p></div>{% endif %}
{% for h in history[:10] %}
<div class="chat-msg agent"><small>{{ h.created_at }}</small><p>{{ h.title or h.body }}</p></div>
{% endfor %}
</div>
<form class="chat-input" @submit.prevent="send">
<input class="form-input" x-model="message" placeholder="Message Herman… (bijv. maak verpakking: doos 120×80×40 voor Chai N Masala project #3)" autocomplete="off" />
<button class="btn btn-primary" type="submit" :disabled="loading">Send</button>
</form>
<div class="card card-compact" style="margin-bottom:1rem;">
<h2 style="font-size:1rem;margin:0 0 .5rem;">Foto genereren (ComfyUI)</h2>
<form class="chat-input" @submit.prevent="generateImage()">
<input class="form-input" x-model="imagePrompt" placeholder="bijv. halal kipnuggets productfoto" autocomplete="off" />
<button class="btn btn-primary" type="submit" :disabled="generating">Genereer</button>
</form>
<div x-show="lastImage" style="margin-top:1rem;">
<img :src="lastImage" alt="Gegenereerd" style="max-width:100%;border-radius:8px;" />
</div>
</div>
<div class="voice-panel" style="min-height:420px">
<div class="voice-chat-log" x-ref="chatLog" style="max-height:360px">
<template x-for="(t, i) in turns" :key="i">
<div class="voice-bubble" :class="t.role === 'user' ? 'voice-bubble-user' : 'voice-bubble-agent'">
<div class="voice-bubble-meta" x-text="t.at + (t.role === 'user' ? ' · jij' : ' · Herman')"></div>
<template x-if="t.meta.agent_label">
<span class="voice-bubble-badge" x-text="t.meta.agent_label + (formatDelegated(t.meta) ? ' → ' + formatDelegated(t.meta) : '')"></span>
</template>
<p x-text="t.text"></p>
<img x-show="t.meta.image_url" :src="t.meta.image_url" style="max-width:100%;margin-top:.5rem;border-radius:8px;" />
</div>
</template>
{% for h in history[:6] %}
<div class="voice-bubble voice-bubble-agent">
<div class="voice-bubble-meta">{{ h.created_at }}</div>
<p>{{ h.title or h.body }}</p>
</div>
{% endfor %}
</div>
<div class="voice-live-preview" :class="{ 'is-interim': liveInterim }" x-show="livePreview || recording" style="margin:0.75rem 0">
<strong x-text="recording ? 'Live preview' : 'Transcript'"></strong>
<span x-text="livePreview || '…'"></span>
</div>
<div class="voice-toggles" style="margin-bottom:0.75rem">
<label><input type="checkbox" x-model="speakReply" /> Antwoord voorlezen</label>
<label><input type="checkbox" x-model="handsFreeMode" /> Hands-free na antwoord</label>
</div>
<div class="voice-manual-row">
<button type="button" class="ha-mic voice-mic-inline" :class="micClass()"
@mousedown.prevent="onMicDown()" @mouseup.prevent="onMicUp()"
@touchstart.prevent="onMicDown()" @touchend.prevent="onMicUp()"
@click.prevent="onMicClick()" title="Microfoon">🎙️</button>
<input class="form-input" x-model="manualText" placeholder="Typ of gebruik de microfoon…" @keydown.enter.prevent="sendText()" />
<button class="btn btn-primary" @click="sendText()" :disabled="busy || !manualText.trim()">Send</button>
</div>
<p class="ha-status" x-show="statusDetail" x-text="statusDetail" style="margin-top:0.5rem"></p>
</div>
<div class="ei-modal-root voice-results-modal" x-show="resultsOpen" x-cloak @click.self="closeResultsModal()">
<div class="ei-modal-card voice-results-card" @click.stop>
<h3 x-text="resultsTitle"></h3>
<p><strong x-text="resultsTotal"></strong> resultaten</p>
<div class="voice-results-table-wrap">
<table class="voice-results-table">
<thead><tr><th>Naam</th><th>Type</th><th>Stad</th><th>Contact</th></tr></thead>
<tbody>
<template x-for="(e, i) in resultsEntities" :key="i">
<tr>
<td><a :href="'/export-intel?open=' + e.id" x-text="e.name"></a></td>
<td x-text="entityTypeLabel(e.entity_type)"></td>
<td x-text="e.city || '—'"></td>
<td x-text="e.primary_email || e.primary_phone || '—'"></td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="ei-modal-actions">
<a class="btn btn-sm" :href="resultsOpenUrl">Wereldexport</a>
<button type="button" class="btn btn-sm btn-pulse" @click="closeResultsModal()">Sluiten</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function esc(s) { const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }
function hermanChat() {
return {
message: '',
imagePrompt: '',
loading: false,
generating: false,
lastImage: '',
async send() {
if (!this.message.trim()) return;
this.loading = true;
const msg = this.message;
try {
const r = await Cockpit.api('/api/herman/chat', { method: 'POST', body: JSON.stringify({ message: msg }) });
const log = document.getElementById('chat-log');
log.innerHTML += '<div class="chat-msg user"><p>' + esc(msg) + '</p></div>';
let badge = esc(r.agent_label || 'Herman');
if (r.delegated_agents && r.delegated_agents.length > 1) {
badge += ' · ' + esc(r.delegated_agents.join(', '));
}
let html = '<div class="chat-msg agent"><span class="badge">' + badge + '</span><p>' + esc(r.reply || '') + '</p>';
if (r.image_url) {
html += '<img src="' + r.image_url + '" alt="Gegenereerd" style="max-width:100%;margin-top:.5rem;border-radius:8px;" />';
this.lastImage = r.image_url;
}
if (r.packaging_studio_url) {
html += '<p style="margin-top:.5rem"><a class="btn btn-sm" href="' + esc(r.packaging_studio_url) + '">📦 Open Packaging Studio</a>';
if (r.packaging_pdf_url) html += ' <a class="btn btn-sm btn-ghost" href="' + esc(r.packaging_pdf_url) + '" target="_blank">PDF</a>';
html += '</p>';
}
html += '</div>';
log.innerHTML += html;
this.message = '';
} catch (e) { Cockpit.toast(e.message, 'error'); }
this.loading = false;
},
async generateImage() {
if (!this.imagePrompt.trim()) return;
this.generating = true;
this.lastImage = '';
try {
const r = await Cockpit.api('/api/ai/generate-image', {
method: 'POST',
body: JSON.stringify({ prompt: this.imagePrompt, width: 512, height: 512, steps: 15 })
});
if (r.proxy_url) {
this.lastImage = r.proxy_url;
const log = document.getElementById('chat-log');
log.innerHTML += '<div class="chat-msg agent"><span class="badge">Design</span><p>Afbeelding klaar: ' + esc(this.imagePrompt) + '</p><img src="' + r.proxy_url + '" alt="Gegenereerd" style="max-width:100%;margin-top:.5rem;border-radius:8px;" /></div>';
}
Cockpit.toast('Afbeelding gegenereerd', 'success');
} catch (e) { Cockpit.toast(e.message, 'error'); }
this.generating = false;
}
};
}
</script>
<script src="/static/js/herman-assistant.js?v=1"></script>
<style>.voice-mic-inline{width:44px;height:44px;border-radius:50%;font-size:1rem;flex-shrink:0}</style>
{% endblock %}