(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 = `
Energy Diagnostics
Zapytaj konsultanta
`;
const box = document.createElement('div');
box.id = 'fec-box';
box.innerHTML = `
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.
`;
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);
});
})();