113 lines
5.4 KiB
HTML
113 lines
5.4 KiB
HTML
{% 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="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="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 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 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 %}
|