<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Voice Chat Agent</title>
    <style>
        * { box-sizing: border-box; }
        body {
            font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
            margin: 0;
            min-height: 100vh;
            background: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 100%);
            color: #1e293b;
            padding: 2rem 1rem;
        }
        .hero {
            max-width: 480px;
            margin: 0 auto 2rem;
            background: #fff;
            border-radius: 16px;
            box-shadow: 0 4px 24px rgba(0,0,0,0.08);
            padding: 2.5rem;
            text-align: center;
        }
        .hero h1 { font-size: 1.5rem; font-weight: 700; margin: 0 0 0.5rem; color: #0f172a; }
        .hero .subtitle { color: #64748b; font-size: 0.95rem; margin-bottom: 1.75rem; }
        .btn {
            display: inline-block;
            background: #4f46e5;
            color: #fff;
            border: none;
            padding: 0.75rem 1.5rem;
            border-radius: 10px;
            font-weight: 600;
            font-size: 0.95rem;
            cursor: pointer;
            transition: background 0.2s;
        }
        .btn:hover { background: #4338ca; }
        .btn-agent { background: #0d9488; }
        .btn-agent:hover { background: #0f766e; }
        .hero .links { margin-top: 1.75rem; padding-top: 1.5rem; border-top: 1px solid #e2e8f0; font-size: 0.875rem; color: #64748b; }
        .hero .links a { color: #4f46e5; text-decoration: none; }
        .hero .links a:hover { text-decoration: underline; }

        #chatPanel { display: none; max-width: 56rem; margin: 0 auto; }
        #chatPanel.visible { display: block; }
        .alert { padding: 0.75rem 1rem; border-radius: 0.5rem; margin-bottom: 1rem; }
        .alert-error { background: #fef2f2; border: 1px solid #fecaca; color: #b91c1c; }
        .alert-warning { background: #fefce8; border: 1px solid #fef08a; color: #a16207; }
        .alert-success { background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534; }
        #progressSection { display: none; margin-bottom: 1rem; }
        #progressSection.visible { display: block; }
        .progress-box { border-radius: 0.5rem; border: 1px solid #e2e8f0; background: #fff; padding: 0.75rem 1rem; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
        .progress-header { display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; margin-bottom: 0.375rem; }
        .progress-header span:first-child { font-size: 0.75rem; font-weight: 500; color: #64748b; }
        .progress-header span:last-child { font-size: 0.75rem; color: #475569; }
        .progress-bar { height: 6px; background: #f1f5f9; border-radius: 9999px; overflow: hidden; margin-top: 0.375rem; }
        .progress-fill { height: 100%; background: #4f46e5; border-radius: 9999px; transition: width 0.3s; }
        .progress-tags { margin-top: 0.5rem; display: flex; flex-wrap: wrap; gap: 0.25rem 0.5rem; }
        .progress-tag { font-size: 0.75rem; padding: 0.125rem 0.375rem; border-radius: 0.25rem; }
        .progress-tag.done { background: #dcfce7; color: #166534; }
        .progress-tag.pending { background: #f1f5f9; color: #64748b; }
        .chat-box { background: #fff; border-radius: 0.5rem; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1); padding: 1.5rem; height: 24rem; overflow-y: auto; margin-bottom: 1.5rem; }
        .chat-empty { text-align: center; color: #64748b; margin-top: 2rem; }
        .message { margin-bottom: 1rem; }
        .message.user { display: flex; justify-content: flex-end; }
        .message.assistant { display: flex; justify-content: flex-start; }
        .bubble { max-width: 20rem; padding: 0.5rem 1rem; border-radius: 0.5rem; font-size: 0.875rem; }
        .message.user .bubble { background: #3b82f6; color: #fff; }
        .message.assistant .bubble { background: #e2e8f0; color: #1e293b; }
        .bubble .role { font-weight: 600; margin-bottom: 0.25rem; }
        .bubble .time { font-size: 0.75rem; opacity: 0.8; margin-top: 0.25rem; }
        .typing { display: flex; gap: 0.25rem; padding: 0.5rem 1rem; }
        .typing span { width: 6px; height: 6px; background: #64748b; border-radius: 50%; animation: pulse 1.4s ease-in-out infinite; }
        .typing span:nth-child(2) { animation-delay: 0.2s; }
        .typing span:nth-child(3) { animation-delay: 0.4s; }
        @keyframes pulse { 0%, 80%, 100% { opacity: 0.4; } 40% { opacity: 1; } }
        .recorder-wrap { display: flex; flex-direction: column; align-items: center; }
        .btn-mic {
            width: 5rem; height: 5rem; border-radius: 50%; border: none; cursor: pointer;
            display: flex; align-items: center; justify-content: center;
            transition: transform 0.2s, background 0.2s; box-shadow: 0 4px 14px rgba(0,0,0,0.15);
        }
        .btn-mic:hover:not(:disabled) { transform: scale(1.05); }
        .btn-mic:disabled { opacity: 0.5; cursor: not-allowed; }
        .btn-mic.recording { background: #ef4444; }
        .btn-mic.recording:hover { background: #dc2626; }
        .btn-mic:not(.recording) { background: #3b82f6; }
        .btn-mic:not(.recording):hover { background: #2563eb; }
        .btn-mic svg { width: 2rem; height: 2rem; color: #fff; }
        .recorder-label { margin-top: 1rem; color: #475569; font-size: 0.875rem; }
        .status-line { text-align: center; margin-top: 1rem; color: #64748b; font-size: 0.875rem; }
    </style>
</head>
<body>
    <div class="hero" id="hero">
        <h1>Voice Chat Agent</h1>
        <p class="subtitle">Start a conversation with the AI using your voice</p>
        <div style="display: flex; flex-wrap: wrap; gap: 0.75rem; justify-content: center; margin-bottom: 1.25rem;">
            <button type="button" class="btn" id="btnOpenChat">Open Voice Chat</button>
        </div>
        <div class="links">
            <a href="<?php echo e(url('/api/health')); ?>">API Health</a>
        </div>
    </div>

    <div id="chatPanel">
        <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; flex-wrap: wrap; gap: 0.5rem; padding: 0.5rem 0; border-bottom: 1px solid #e2e8f0;">
            <a href="<?php echo e(route('agent-instructions.index')); ?>" class="btn btn-agent" style="padding: 0.5rem 1rem; font-size: 0.9rem; text-decoration: none;">✎ Agent instructions</a>
            <span style="font-size: 0.875rem; color: #64748b;">Voice chat</span>
        </div>
        <div id="connectionError" class="alert alert-error" style="display: none;"></div>
        <div id="connectionWaiting" class="alert alert-warning">Connecting...</div>
        <div id="connectionOk" class="alert alert-success" style="display: none;">✓ Connected</div>
        <div id="progressSection">
            <div class="progress-box">
                <div class="progress-header"><span>Progress</span><span id="progressLabel">—</span></div>
                <div class="progress-bar"><div id="progressFill" class="progress-fill" style="width: 0%"></div></div>
                <div id="progressTags" class="progress-tags"></div>
            </div>
        </div>
        <div class="chat-box" id="chatBox">
            <div id="chatEmpty" class="chat-empty">Speak after the mic opens.</div>
            <div id="chatMessages"></div>
            <div id="chatTyping" class="message assistant" style="display: none;">
                <div class="bubble"><div class="typing"><span></span><span></span><span></span></div></div>
            </div>
        </div>
        <div id="voiceErrorBox" class="alert alert-error" style="display: none; max-height: 200px; overflow: auto;">
            <div style="font-weight: 600; margin-bottom: 6px;">Error (you can copy the text below):</div>
            <pre id="voiceErrorText" style="margin: 0; white-space: pre-wrap; word-break: break-word; font-size: 12px; max-height: 120px; overflow: auto;"></pre>
            <div style="margin-top: 8px;">
                <button type="button" id="voiceErrorCopy" class="btn" style="padding: 4px 12px; font-size: 12px;">Copy error</button>
                <button type="button" id="voiceErrorDismiss" style="margin-left: 8px; padding: 4px 12px; font-size: 12px; background: #94a3b8; color: #fff; border: none; border-radius: 6px; cursor: pointer;">Dismiss</button>
            </div>
        </div>
        <div class="recorder-wrap">
            <button type="button" id="btnMic" class="btn-mic" disabled title="Microphone">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M7 4a3 3 0 016 0v4a3 3 0 11-6 0V4zm4 10.93A7.001 7.001 0 0017 8a1 1 0 10-2 0A5 5 0 015 8a1 1 0 00-2 0 7.001 7.001 0 006 6.93V17H6a1 1 0 100 2h8a1 1 0 100-2h-3v-2.07z" clip-rule="evenodd"/></svg>
            </button>
            <p class="recorder-label" id="recorderLabel">Click to start recording</p>
            <p class="status-line" id="statusLine" style="display: none;"></p>
        </div>
    </div>

    <script>
(function () {
    var pathname = window.location.pathname.replace(/\/$/, '') || '';
    var base = pathname.replace(/\/index\.php.*$/, '') || '';
    var API = window.location.origin + base + '/api.php';
    const STATE_LABELS = { greeting: 'Started', need: 'Car preference', budget: 'Budget', timeline: 'When to buy', finance: 'Finance', contact_name: 'Your name', contact_number: 'Phone', contact_city: 'City', summary: 'Summary', complete: 'Done' };
    const FIELD_LABELS = { car_type: 'Car type', usage: 'Usage', budget: 'Budget', timeline: 'Timeline', finance_required: 'Finance', exchange_vehicle: 'Exchange', name: 'Name', mobile_number: 'Phone', city: 'City' };
    const FIELDS_ORDER = ['car_type', 'usage', 'budget', 'timeline', 'finance_required', 'exchange_vehicle', 'name', 'mobile_number', 'city'];

    let sessionId = null;
    let isConnected = false;
    let stream = null;
    let recorder = null;
    let chunks = [];
    let resolveRecord = null;
    let conversationActive = false;
    let audioContext = null;
    let analyser = null;
    let sourceNode = null;
    let monitorInterval = null;
    let silenceStartedAt = null;

    const SILENCE_RMS_THRESHOLD = 0.02;
    const SILENCE_DURATION_MS = 1700;
    const MONITOR_INTERVAL_MS = 150;

    const el = (id) => document.getElementById(id);
    const show = (id, on) => { const e = el(id); if (e) e.style.display = on ? '' : 'none'; };
    const setText = (id, text) => { const e = el(id); if (e) e.textContent = text; };

    function stripHtml(html) {
        if (!html || typeof html !== 'string') return '';
        var div = document.createElement('div');
        div.innerHTML = html;
        var text = (div.textContent || div.innerText || '').replace(/\s+/g, ' ').trim();
        return text.slice(0, 1500);
    }

    function extractErrorFromHtml(html) {
        if (!html || typeof html !== 'string') return 'Unknown error';
        var m = html.match(/class="exception-message-wrapper"[^>]*>[\s\S]*?<span[^>]*>([\s\S]*?)<\/span>/i)
            || html.match(/class="message"[^>]*>([^<]+)</i)
            || html.match(/<title>([^<]+)<\/title>/i)
            || html.match(/Exception[:\s]+([^<\n]+)/i)
            || html.match(/Error[:\s]+([^<\n]+)/i);
        if (m && m[1]) return m[1].replace(/\s+/g, ' ').trim().slice(0, 400);
        var text = stripHtml(html);
        var lines = text.split(/\s{2,}|\n/).filter(function (l) { return l.length > 5; });
        for (var i = 0; i < lines.length; i++) {
            var line = lines[i].trim();
            if (line.indexOf('Exception') !== -1 || line.indexOf('Error') !== -1 || line.indexOf(' in ') !== -1) return line.slice(0, 400);
        }
        return (lines[0] || text).slice(0, 500) || 'Unknown error (see full response in Laravel log)';
    }

    function showError(msg) {
        var plain = stripHtml(msg);
        if (!plain) plain = String(msg);
        el('voiceErrorText').textContent = plain;
        el('voiceErrorBox').style.display = 'block';
    }

    el('voiceErrorCopy').addEventListener('click', function () {
        var t = el('voiceErrorText').textContent;
        if (navigator.clipboard && navigator.clipboard.writeText) {
            navigator.clipboard.writeText(t).then(function () { alert('Copied to clipboard'); });
        } else {
            var ta = document.createElement('textarea'); ta.value = t; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); alert('Copied to clipboard');
        }
    });
    el('voiceErrorDismiss').addEventListener('click', function () { el('voiceErrorBox').style.display = 'none'; });

    function setConnection(ok, errorMsg) {
        isConnected = ok;
        show('connectionError', !ok && errorMsg);
        show('connectionWaiting', !ok && !errorMsg);
        show('connectionOk', ok);
        if (errorMsg) el('connectionError').textContent = 'Connection Error: ' + errorMsg;
        el('btnMic').disabled = !ok;
    }

    function setConversationState(active) {
        conversationActive = active;
        const btn = el('btnMic');
        if (btn) btn.title = active ? 'Turn microphone OFF' : 'Turn microphone ON';
        if (!active) {
            stopSilenceMonitor();
            setText('recorderLabel', 'Mic is OFF. Click to start conversation');
            el('btnMic').classList.remove('recording');
        }
    }

    function stopSilenceMonitor() {
        silenceStartedAt = null;
        if (monitorInterval) {
            clearInterval(monitorInterval);
            monitorInterval = null;
        }
        if (sourceNode) {
            try { sourceNode.disconnect(); } catch (e) {}
            sourceNode = null;
        }
        analyser = null;
        if (audioContext) {
            audioContext.close().catch(function () {});
            audioContext = null;
        }
    }

    function startSilenceMonitor(streamToUse) {
        stopSilenceMonitor();
        const AudioCtx = window.AudioContext || window.webkitAudioContext;
        if (!AudioCtx) return;

        audioContext = new AudioCtx();
        analyser = audioContext.createAnalyser();
        analyser.fftSize = 2048;
        sourceNode = audioContext.createMediaStreamSource(streamToUse);
        sourceNode.connect(analyser);

        const data = new Uint8Array(analyser.fftSize);

        monitorInterval = setInterval(function () {
            if (!conversationActive || !recorder || recorder.state !== 'recording' || !analyser) return;

            analyser.getByteTimeDomainData(data);
            let sum = 0;
            for (let i = 0; i < data.length; i++) {
                const v = (data[i] - 128) / 128;
                sum += v * v;
            }
            const rms = Math.sqrt(sum / data.length);
            const now = Date.now();

            if (rms < SILENCE_RMS_THRESHOLD) {
                if (!silenceStartedAt) silenceStartedAt = now;
                if (now - silenceStartedAt >= SILENCE_DURATION_MS) {
                    stopRecording();
                }
            } else {
                silenceStartedAt = null;
            }
        }, MONITOR_INTERVAL_MS);
    }

    async function initSession() {
        try {
            const healthRes = await fetch(API + '/health');
            if (!healthRes.ok) throw new Error('Health: ' + healthRes.status + ' ' + healthRes.statusText);
            const sessionRes = await fetch(API + '/voice/session');
            if (!sessionRes.ok) throw new Error('Session: ' + sessionRes.status + ' ' + sessionRes.statusText);
            const data = await sessionRes.json();
            sessionId = data.session_id;
            if (sessionId) setConnection(true); else setConnection(false, 'No session ID');
        } catch (e) {
            setConnection(false, e.message || 'Could not connect');
        }
    }

    function addMessage(role, content, timestamp) {
        const empty = el('chatEmpty');
        const list = el('chatMessages');
        if (empty) empty.style.display = 'none';
        const div = document.createElement('div');
        div.className = 'message ' + role;
        const time = timestamp ? new Date(timestamp).toLocaleTimeString() : '';
        div.innerHTML = '<div class="bubble"><div class="role">' + (role === 'user' ? 'You' : 'AI Assistant') + '</div><div class="content">' + escapeHtml(content) + '</div><div class="time">' + time + '</div></div>';
        list.appendChild(div);
        el('chatBox').scrollTop = el('chatBox').scrollHeight;
    }

    function escapeHtml(s) {
        const d = document.createElement('div');
        d.textContent = s;
        return d.innerHTML;
    }

    function updateProgress(agentState) {
        if (!agentState) { el('progressSection').classList.remove('visible'); return; }
        el('progressSection').classList.add('visible');
        const d = agentState.collected_data || {};
        const next = agentState.next_state || '';
        const count = FIELDS_ORDER.filter(k => d[k] != null && d[k] !== '').length;
        const pct = Math.round((count / FIELDS_ORDER.length) * 100);
        setText('progressLabel', count + '/' + FIELDS_ORDER.length + ' · ' + (STATE_LABELS[next] || next));
        el('progressFill').style.width = pct + '%';
        el('progressTags').innerHTML = FIELDS_ORDER.map(k => {
            const done = d[k] != null && d[k] !== '';
            return '<span class="progress-tag ' + (done ? 'done' : 'pending') + '">' + (done ? '✓' : '·') + ' ' + (FIELD_LABELS[k] || k) + '</span>';
        }).join('');
    }

    function blobToBase64(blob) {
        return new Promise((resolve, reject) => {
            const r = new FileReader();
            r.onloadend = () => resolve(r.result.split(',')[1]);
            r.onerror = reject;
            r.readAsDataURL(blob);
        });
    }

    function playAudioBase64(base64) {
        if (!base64 || base64.length < 10) return Promise.resolve();
        return new Promise(function (resolve, reject) {
            var a = new Audio('data:audio/mpeg;base64,' + base64);
            a.onended = function () { resolve(); };
            a.onerror = function () { resolve(); };
            a.play().catch(function () { resolve(); });
        });
    }

    async function processAudio(audioBase64, mimeType, languageHint) {
        if (!sessionId) return;
        mimeType = mimeType || 'audio/webm';
        languageHint = languageHint || '';
        show('chatTyping', true);
        el('btnMic').disabled = true;
        setText('statusLine', 'Processing your message...');
        show('statusLine', true);
        try {
            setText('statusLine', 'Sending audio...');
            var res = await fetch(API + '/voice/process-sync', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
                body: JSON.stringify({ audio: audioBase64, session_id: sessionId, mime_type: mimeType, language_hint: languageHint })
            });
            var body = await res.text();
            if (!res.ok) {
                var errText = res.statusText;
                var fullPlain = body;
                try {
                    var j = JSON.parse(body);
                    if (j.message) errText = j.message;
                    else if (j.error) errText = j.error;
                    fullPlain = errText || body;
                    if (j.exception || j.file || j.line) {
                        fullPlain = (fullPlain || '') + (j.exception ? '\nException: ' + j.exception : '') + (j.file ? '\nFile: ' + j.file : '') + (j.line ? '\nLine: ' + j.line : '');
                    }
                } catch (_) {
                    if (body.length && body.trim().charAt(0) === '<') {
                        errText = extractErrorFromHtml(body);
                        fullPlain = 'Server ' + res.status + ' - ' + errText + '\n\n' + stripHtml(body).slice(0, 500);
                    }
                }
                showError(fullPlain || ('Server ' + res.status + ': ' + errText));
                show('chatTyping', false);
                return;
            }
            var data;
            try {
                data = JSON.parse(body);
            } catch (_) {
                showError('Invalid response from server');
                show('chatTyping', false);
                return;
            }
            if (data.error) {
                showError(data.error);
                show('chatTyping', false);
                return;
            }
            addMessage('user', data.transcription || '', Date.now());
            addMessage('assistant', data.reply || '', Date.now());
            show('chatTyping', false);
            if (data.agent_state) updateProgress(data.agent_state);
            setText('statusLine', 'Playing response...');
            await playAudioBase64(data.audio || '');
        } catch (e) {
            show('chatTyping', false);
            showError('Error: ' + (e.message || 'Something went wrong'));
        } finally {
            setText('statusLine', ''); show('statusLine', false); el('btnMic').disabled = false;
            if (conversationActive && (!recorder || recorder.state !== 'recording')) {
                startRecording();
            }
        }
    }

    function startRecordingWithStream(streamToUse) {
        stream = streamToUse;
        chunks = [];
        var mime = 'audio/webm';
        if (MediaRecorder.isTypeSupported('audio/webm;codecs=opus')) {
            mime = 'audio/webm;codecs=opus';
        } else if (MediaRecorder.isTypeSupported('audio/mp4')) {
            mime = 'audio/mp4';
        }
        try {
            recorder = new MediaRecorder(stream, { mimeType: mime, audioBitsPerSecond: 128000 });
        } catch (e) {
            recorder = new MediaRecorder(stream);
            mime = recorder.mimeType || 'audio/webm';
        }
        recorder.ondataavailable = function (e) {
            if (e.data && e.data.size > 0) chunks.push(e.data);
        };
        recorder.onstop = function () {
            // Defer so final dataavailable (from stop()) is processed first in all browsers
            setTimeout(function () {
                var blob = new Blob(chunks, { type: mime });
                if (resolveRecord) resolveRecord(blob);
            }, 200);
        };
        recorder.onerror = function (e) {
            if (resolveRecord) resolveRecord(null);
        };
        recorder.start(250);
        el('btnMic').classList.add('recording');
        startSilenceMonitor(streamToUse);
        setText('recorderLabel', 'Mic is ON. Speak naturally. Silence auto-sends your message.');
    }

    async function startRecording() {
        if (!conversationActive) return;
        try {
            var streamToUse = await navigator.mediaDevices.getUserMedia({ audio: true });
            startRecordingWithStream(streamToUse);
        } catch (e) {
            showError('Microphone: ' + e.message);
            setConversationState(false);
        }
    }

    function stopRecording() {
        if (!recorder || recorder.state !== 'recording') return;
        stopSilenceMonitor();
        el('btnMic').classList.remove('recording');
        setText('recorderLabel', conversationActive ? 'Listening will restart after response...' : 'Mic is OFF. Click to start conversation');
        var rec = recorder;
        var streamToStop = stream;
        recorder = null;
        stream = null;
        const shouldProcessAudio = conversationActive;
        new Promise(function (r) { resolveRecord = r; }).then(async function (blob) {
            resolveRecord = null;
            if (streamToStop) streamToStop.getTracks().forEach(function (t) { t.stop(); });
            if (!shouldProcessAudio) return;
            if (!blob || blob.size < 1000) {
                showError('Recording too short or no audio captured. Please speak clearly for 3–5 seconds, then click the mic to stop.');
                el('btnMic').disabled = false;
                if (conversationActive) startRecording();
                return;
            }
            var base64 = await blobToBase64(blob);
            var mime = (blob.type && blob.type.indexOf('audio/') === 0) ? blob.type : 'audio/webm';
            var langHint = (navigator.language || '').split('-')[0] || '';
            await processAudio(base64, mime, langHint);
        });
        try {
            if (rec.requestData) rec.requestData();
        } catch (_) {}
        rec.stop();
    }

    el('btnMic').addEventListener('click', function () {
        if (!isConnected) return;

        if (conversationActive) {
            setConversationState(false);
            if (recorder && recorder.state === 'recording') stopRecording();
            return;
        }

        setConversationState(true);
        startRecording();
    });

    el('btnOpenChat').addEventListener('click', async function () {
        el('hero').style.display = 'none';
        el('chatPanel').classList.add('visible');
        try {
            await initSession();
        } catch (e) {
            await initSession();
            if (e.name && e.name !== 'NotAllowedError') showError('Microphone: ' + e.message);
        }
        setConversationState(false);
    });
})();
    </script>
</body>
</html>
<?php /**PATH C:\xampp\htdocs\voiceagent\resources\views/welcome.blade.php ENDPATH**/ ?>