Files

169 lines
7.9 KiB
HTML

{% extends "base.html" %}
{% block title %}Voice Live · Herman{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="/static/css/export-intel.css?v=11" />
<link rel="stylesheet" href="/static/css/voice-live.css?v=2" />
{% endblock %}
{% block content %}
<div class="voice-live-page" x-data="VoiceLive.page()" x-init="init()" @beforeunload="destroy()">
<header class="page-header">
<div>
<h1>Voice Live <span class="live-badge">Realtime</span></h1>
<p class="subtitle page-tip">Spreek met Herman — bevestiging bij zoekopdrachten, popup met Export Intel resultaten.</p>
</div>
<div class="retail-actions">
<button type="button" class="btn btn-sm" @click="clearSession()">Sessie wissen</button>
<a class="btn btn-sm btn-pulse" href="/herman">Herman chat</a>
<a class="btn btn-sm" href="/agents">Agents</a>
</div>
</header>
<!-- Bevestigingsbalk -->
<div class="voice-confirm-bar" x-show="pendingAction" x-cloak>
<div class="voice-confirm-text">
<strong>Bevestiging nodig</strong>
<span x-text="pendingAction ? (pendingAction.type === 'webbuilder_build' ? ('Agy website: ' + (pendingAction.project || '')) : ('Zoeken: ' + (pendingAction.entity_label || 'bedrijven') + ' 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="voice-live-grid">
<section class="voice-panel voice-mic-zone">
<button
type="button"
class="voice-mic-btn"
:class="micClass()"
@mousedown.prevent="onMicDown()"
@mouseup.prevent="onMicUp()"
@mouseleave="recording && !handsFreeMode && onMicUp()"
@touchstart.prevent="onMicDown()"
@touchend.prevent="onMicUp()"
@click.prevent="onMicClick()"
title="PTT · hands-free toggle · Spatiebalk"
>🎙️</button>
<div class="voice-status" x-text="status === 'listening' ? (handsFreeMode ? '● HANDS-FREE' : '● LIVE') : (busy ? '⏳ Verwerken…' : 'Gereed')"></div>
<p class="voice-status-sub" x-text="statusDetail"></p>
<div class="voice-live-preview" :class="{ 'is-interim': liveInterim }">
<strong x-text="recording ? 'Live preview (browser)' : 'Whisper transcript'"></strong>
<span x-text="livePreview || 'Je spraak verschijnt hier terwijl je praat…'"></span>
</div>
<div class="voice-toggles">
<label>
<input type="checkbox" x-model="handsFreeMode" @change="onHandsFreeChange()" />
<span>Hands-free (auto-luisteren na antwoord)</span>
</label>
<label>
<input type="checkbox" x-model="conversationMode" />
<span>Gespreksmodus (auto → Herman na opname)</span>
</label>
<label>
<input type="checkbox" x-model="speakReply" />
<span>Antwoord voorlezen (browser TTS)</span>
</label>
</div>
<div class="voice-manual-row">
<input class="form-input" type="text" placeholder="Of typ: ja / nee / opdracht…" x-model="manualText" @keydown.enter.prevent="sendManual()" />
<button type="button" class="btn btn-sm btn-pulse" @click="sendManual()" :disabled="busy || !manualText.trim()"></button>
</div>
</section>
<section class="voice-panel">
<div class="voice-chat-head">
<h2>Gesprek met Herman</h2>
<span class="ei-filter-count" x-text="turns.length + ' berichten'"></span>
</div>
<div class="voice-chat-log" id="voice-chat-log">
<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.role === 'agent' && 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>
<p class="voice-bubble-meta" x-show="t.meta.routing_reason" x-text="t.meta.routing_reason"></p>
</div>
</template>
<p x-show="!turns.length" class="page-tip" style="margin:1rem 0">Vraag bv. “zoek alle distributeurs in Dubai” — Herman vraagt eerst om bevestiging.</p>
</div>
</section>
<section class="voice-panel">
<h3 class="voice-side-head">Achter de schermen</h3>
<ul class="voice-pipeline">
<template x-for="(s, i) in pipeline" :key="i">
<li :class="{ 'is-running': s.status === 'running', 'is-error': s.status === 'error' }">
<span class="vl-phase" x-text="s.phase"></span>
<span class="vl-label" x-text="s.label"></span>
<span class="vl-detail" x-show="s.detail" x-text="s.detail"></span>
</li>
</template>
</ul>
<p x-show="!pipeline.length" class="page-tip" style="font-size:0.72rem">Pipeline-stappen per beurt.</p>
<h3 class="voice-side-head" style="margin-top:0.75rem">Live agent-feed</h3>
<div class="voice-feed">
<template x-for="(f, i) in feed" :key="i">
<div class="voice-feed-item">
<span class="vl-ts" x-text="f.at"></span>
<strong x-text="f.agent"></strong>
<span x-text="f.message"></span>
</div>
</template>
</div>
</section>
</div>
<!-- Export Intel resultaten popup -->
<div class="ei-modal-root voice-results-modal" x-show="resultsOpen" x-cloak @click.self="closeResultsModal()" @keydown.escape.window="closeResultsModal()">
<div class="ei-modal-card voice-results-card" @click.stop>
<h3 x-text="resultsTitle"></h3>
<p><strong x-text="resultsTotal"></strong> resultaten · Export Intel</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?tab=distributors&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>
<p x-show="!resultsEntities.length" class="page-tip">Geen resultaten in deze selectie.</p>
</div>
<div class="ei-modal-actions">
<a class="btn btn-sm" :href="resultsOpenUrl" target="_blank">Open in Wereldexport</a>
<button type="button" class="btn btn-sm btn-pulse" @click="closeResultsModal()">Sluiten</button>
</div>
</div>
</div>
<div class="ei-modal-root voice-results-modal" x-show="webbuilderOpen" x-cloak @click.self="closeWebbuilderModal()">
<div class="ei-modal-card voice-results-card" @click.stop>
<h3 x-text="webbuilderTitle"></h3>
<p><strong x-text="webbuilderProject"></strong> · Agy / Antigravity</p>
<p class="page-tip">Preview: <a :href="webbuilderPreview" target="_blank" rel="noopener" x-text="webbuilderPreview"></a></p>
<div class="ei-modal-actions">
<a class="btn btn-sm btn-pulse" :href="webbuilderAgentsUrl">Agents terminal</a>
<a class="btn btn-sm" :href="webbuilderPreview" target="_blank" rel="noopener">Open preview</a>
<button type="button" class="btn btn-sm" @click="closeWebbuilderModal()">Sluiten</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/voice-live.js?v=3"></script>
{% endblock %}