(function () { 'use strict'; const script = document.currentScript; const api = (script && script.dataset && script.dataset.feiginApi) || window.FEIGIN_ENERGY_API || window.location.origin; const avatar = api + '/static/feigin-energy-konsultant-avatar.svg?v=5'; const cssHref = api + '/static/widget-v2.css?v=5'; const css = document.createElement('link'); css.rel = 'stylesheet'; css.href = cssHref; document.head.appendChild(css); if (document.getElementById('fec-box')) return; const button = document.createElement('button'); button.id = 'fec-btn'; button.type = 'button'; button.innerHTML = ` Feigin Energy Konsultant Energy Diagnostics Zapytaj konsultanta `; const box = document.createElement('div'); box.id = 'fec-box'; box.innerHTML = `
Feigin Energy Konsultant
Feigin Electric
Energy Konsultant
Wirtualny ekspert energetyczny
tekst i rozmowa głosowa
EMSpomiary i dane
ECODoptymalizacja
PV / PQnapięcie i jakość
🎙️ Słucham. Powiedz, jaki problem ma Twoja firma.
Zostawić kontakt do analizy?
Dane trafiają do Feigin Electric jako lead techniczno-sprzedażowy.
Feigin Electric · feiginelectric.pl · konsultacja informacyjna, bez gwarancji oszczędności bez danych, pomiarów i analizy.
`; document.body.append(button, box); const $ = (selector) => box.querySelector(selector); const log = $('#fec-log'); const input = $('#fec-input'); const form = $('#fec-form'); const sendButton = $('#fec-send'); const micButton = $('#fec-mic'); const soundButton = $('#fec-sound'); const voiceStatus = $('#fec-voice-status'); const closeButton = $('#fec-close'); const leadName = $('#fec-lead-name'); const leadPhone = $('#fec-lead-phone'); const leadEmail = $('#fec-lead-email'); const leadConsent = $('#fec-lead-consent-check'); const leadSend = $('#fec-lead-send'); let conversationId = null; let lastLead = null; let busy = false; let voiceEnabled = true; let recognition = null; let listening = false; let finalTranscript = ''; let micPermissionReady = false; let voices = []; const maleNames = ['krzysztof', 'marek', 'jacek', 'zbigniew', 'adam', 'jan', 'male']; const femaleNames = ['zosia', 'paulina', 'agnieszka', 'ewa', 'maja', 'female']; function loadVoices() { if (!window.speechSynthesis) return []; voices = window.speechSynthesis.getVoices() || []; return voices; } function chooseMalePolishVoice() { const available = voices.length ? voices : loadVoices(); const polish = available.filter(v => /^pl([_-]|$)/i.test(v.lang || '')); for (const token of maleNames) { const match = polish.find(v => (v.name || '').toLowerCase().includes(token)); if (match) return match; } const neutralPolish = polish.find(v => !femaleNames.some(token => (v.name || '').toLowerCase().includes(token))); return neutralPolish || polish[0] || null; } function speak(text, attempt) { if (!voiceEnabled || !window.speechSynthesis || !text) return; const tryNo = attempt || 0; const selected = chooseMalePolishVoice(); if (!selected && tryNo < 6) { setTimeout(() => speak(text, tryNo + 1), 200); return; } window.speechSynthesis.cancel(); const utterance = new SpeechSynthesisUtterance(String(text).replace(/https?:\/\/\S+/g, '').slice(0, 1800)); utterance.lang = 'pl-PL'; utterance.rate = 0.96; utterance.pitch = selected ? 0.90 : 0.78; if (selected) utterance.voice = selected; window.speechSynthesis.speak(utterance); } if (window.speechSynthesis) { loadVoices(); window.speechSynthesis.onvoiceschanged = loadVoices; } function addMessage(text, who, speakIt) { const row = document.createElement('div'); row.className = 'fec-row ' + (who === 'user' ? 'fec-row-user' : 'fec-row-bot'); if (who !== 'user') { const img = document.createElement('img'); img.className = 'fec-mini-avatar'; img.src = avatar; img.alt = ''; row.appendChild(img); } const bubble = document.createElement('div'); bubble.className = 'fec-msg ' + (who === 'user' ? 'fec-user' : 'fec-bot'); bubble.innerText = text; row.appendChild(bubble); log.appendChild(row); log.scrollTop = log.scrollHeight; if (speakIt) speak(text); return bubble; } async function startGreeting() { if (log.dataset.started) return; log.dataset.started = '1'; try { const response = await fetch(api + '/api/start-message'); const data = await response.json(); addMessage(data.message || 'Dzień dobry. Opisz problem z energią, PV, napięciem, rachunkiem lub mocą bierną.', 'bot', false); } catch (_) { addMessage('Dzień dobry. Opisz problem z energią, PV, napięciem, rachunkiem lub mocą bierną.', 'bot', false); } } function maybeShowLead(data, userText) { if (!data) return; const risk = String(data.risk || ''); const qualification = String(data.qualification || ''); const nextStep = String(data.next_step || ''); const shouldShow = Boolean(data.is_lead) || risk === 'Wysokie' || risk === 'Pilny serwis' || qualification === 'MEASURE' || qualification === 'ANALYSE' || nextStep.includes('pomiar') || nextStep.includes('analiza'); if (!shouldShow) return; lastLead = { topic: String(userText || '').slice(0, 90), category: data.category || 'Inne', problem_description: userText || '', urgency: (risk === 'Wysokie' || risk === 'Pilny serwis') ? 'Wysoka' : 'Średnia', next_step: data.next_step || 'Zebrać dane', source: 'Widget' }; box.classList.add('fec-has-lead'); } async function sendLead() { if (!lastLead) return addMessage('Najpierw opisz problem, potem wyślemy zgłoszenie.', 'bot', true); if (!leadConsent.checked) return addMessage('Do przekazania zgłoszenia potrzebujemy zgody na kontakt.', 'bot', true); const name = leadName.value.trim(); const phone = leadPhone.value.trim(); const email = leadEmail.value.trim(); if (!name || (!phone && !email)) return addMessage('Podaj proszę imię lub nazwę firmy oraz telefon albo email.', 'bot', true); leadSend.disabled = true; leadSend.innerText = 'Wysyłam...'; try { const payload = { ...lastLead, contact: { name_or_company: name, phone, reply_address: email, contact_consent: true, marketing_consent: false } }; const response = await fetch(api + '/api/lead', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) }); if (!response.ok) throw new Error('lead_failed'); await response.json(); box.classList.remove('fec-has-lead'); addMessage('Dziękuję. Zgłoszenie zostało zapisane. Feigin Electric może teraz wrócić do Ciebie z analizą lub propozycją pomiaru.', 'bot', true); } catch (_) { addMessage('Nie udało się zapisać zgłoszenia. Spróbuj ponownie albo wyślij materiały na office@feiginelectric.com.', 'bot', true); } finally { leadSend.disabled = false; leadSend.innerText = 'Wyślij zgłoszenie do Feigin Electric'; } } async function sendMessage(text) { const clean = String(text || '').trim(); if (!clean || busy) return; busy = true; if (window.speechSynthesis) window.speechSynthesis.cancel(); sendButton.disabled = true; sendButton.innerText = 'Czekaj...'; addMessage(clean, 'user', false); input.value = ''; const loading = addMessage('Analizuję zgłoszenie: problem techniczny, ryzyko, brakujące dane i następny krok...', 'bot', false); try { const response = await fetch(api + '/api/chat', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ message: clean, conversation_id: conversationId, source: 'Widget', page_url: window.location.href }) }); if (!response.ok) throw new Error('chat_failed'); const data = await response.json(); conversationId = data.conversation_id || conversationId; loading.innerText = data.answer || 'Nie udało się wygenerować odpowiedzi.'; maybeShowLead(data, clean); speak(loading.innerText); } catch (_) { loading.innerText = 'Błąd połączenia z konsultantem. Spróbuj ponownie za chwilę.'; speak(loading.innerText); } finally { busy = false; sendButton.disabled = false; sendButton.innerText = 'Wyślij'; } } function microphoneErrorMessage(code) { const insideIframe = window.self !== window.top; if (code === 'not-allowed' || code === 'service-not-allowed' || code === 'permission_denied' || code === 'NotAllowedError') { if (insideIframe) return 'Mikrofon jest blokowany przez osadzenie strony. Ramka iframe musi mieć allow="microphone; autoplay".'; return 'Mikrofon nadal jest blokowany. Sprawdź zgodę Chrome i macOS, a następnie odśwież stronę.'; } if (code === 'audio-capture' || code === 'NotFoundError') return 'Chrome nie widzi działającego mikrofonu. Sprawdź wybrane urządzenie wejściowe.'; if (code === 'no-speech') return 'Nie wykryto mowy. Spróbuj ponownie i mów chwilę dłużej.'; if (code === 'insecure') return 'Rozmowa głosowa wymaga bezpiecznego połączenia HTTPS.'; return 'Nie udało się uruchomić rozmowy głosowej. Odśwież stronę i spróbuj ponownie.'; } async function ensureMicrophoneAccess() { if (micPermissionReady) return true; if (!window.isSecureContext) throw new Error('insecure'); if (window.parent && window.parent !== window) { window.parent.postMessage({type: 'FEIGIN_MICROPHONE_REQUIRED'}, '*'); } if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { const stream = await navigator.mediaDevices.getUserMedia({audio: true}); stream.getTracks().forEach(track => track.stop()); } micPermissionReady = true; return true; } const Recognition = window.SpeechRecognition || window.webkitSpeechRecognition; if (Recognition) { recognition = new Recognition(); recognition.lang = 'pl-PL'; recognition.continuous = false; recognition.interimResults = true; recognition.onstart = () => { finalTranscript = ''; listening = true; micButton.classList.add('fec-listening'); micButton.innerText = '■'; voiceStatus.classList.add('fec-active'); }; recognition.onresult = (event) => { let interim = ''; for (let i = event.resultIndex; i < event.results.length; i += 1) { const transcript = event.results[i][0].transcript; if (event.results[i].isFinal) finalTranscript += transcript + ' '; else interim += transcript; } input.value = (finalTranscript + interim).trim(); }; recognition.onerror = (event) => { addMessage(microphoneErrorMessage((event && event.error) || 'unknown'), 'bot', false); }; recognition.onend = () => { const transcript = (finalTranscript || input.value).trim(); listening = false; micButton.classList.remove('fec-listening'); micButton.innerText = '🎙️'; voiceStatus.classList.remove('fec-active'); finalTranscript = ''; if (transcript && !busy) sendMessage(transcript); }; } button.onclick = async () => { box.style.display = box.style.display === 'block' ? 'none' : 'block'; if (box.style.display === 'block') { await startGreeting(); setTimeout(() => input.focus(), 50); } else if (window.speechSynthesis) { window.speechSynthesis.cancel(); } }; closeButton.onclick = () => { box.style.display = 'none'; if (recognition && listening) recognition.stop(); if (window.speechSynthesis) window.speechSynthesis.cancel(); }; soundButton.onclick = () => { voiceEnabled = !voiceEnabled; soundButton.innerText = voiceEnabled ? '🔊' : '🔇'; soundButton.classList.toggle('fec-off', !voiceEnabled); if (!voiceEnabled && window.speechSynthesis) window.speechSynthesis.cancel(); }; form.onsubmit = (event) => { event.preventDefault(); sendMessage(input.value); }; micButton.onclick = async () => { if (!recognition) { addMessage('Ta przeglądarka nie obsługuje rozpoznawania mowy. Użyj aktualnego Chrome lub Edge.', 'bot', false); return; } try { if (listening) { recognition.stop(); return; } await ensureMicrophoneAccess(); recognition.start(); } catch (error) { addMessage(microphoneErrorMessage((error && (error.name || error.message)) || 'unknown'), 'bot', false); } }; leadSend.onclick = sendLead; box.querySelectorAll('.fec-chip').forEach(chip => { chip.onclick = () => sendMessage(chip.innerText); }); })();