(() => {
'use strict';
if (window.__EKS_CHATWOOT_AI_TOOLS__) return;
window.__EKS_CHATWOOT_AI_TOOLS__ = true;
const RELAY_ORIGIN = new URL(document.currentScript?.src || 'https://eks-relay.easyklima.com/chatwoot-ai-tools.js').origin;
const STORE_KEY = 'eksAgentUiToken';
const PANEL_ID = 'eks-ai-translate-panel';
const BUTTON_ID = 'eks-ai-translate-toggle';
const POPOVER_ID = 'eks-ai-translate-popover';
const state = { busy: false, result: '', error: '' };
function ensureStyles() {
if (document.getElementById('eks-ai-translate-styles')) return;
const style = document.createElement('style');
style.id = 'eks-ai-translate-styles';
style.textContent = `
#${BUTTON_ID} {
position: fixed; right: 18px; bottom: 18px; z-index: 2147483000;
border: 0; border-radius: 999px; padding: 10px 14px;
background: #1f6feb; color: white; font: 600 13px system-ui, sans-serif;
box-shadow: 0 8px 22px rgba(15,23,42,.25); cursor: pointer;
}
#${PANEL_ID}, #${POPOVER_ID} {
position: fixed; z-index: 2147483000; background: #fff; color: #0f172a;
border: 1px solid #d7dee8; border-radius: 14px;
box-shadow: 0 18px 48px rgba(15,23,42,.28); font: 13px system-ui, sans-serif;
}
#${PANEL_ID} {
right: 18px; bottom: 66px; width: min(420px, calc(100vw - 36px));
max-height: min(720px, calc(100vh - 92px)); overflow: auto; padding: 14px;
}
#${POPOVER_ID} { right: 18px; top: 90px; width: min(460px, calc(100vw - 36px)); max-height: 55vh; overflow: auto; padding: 12px; }
#${PANEL_ID}[hidden], #${POPOVER_ID}[hidden] { display: none; }
#${PANEL_ID} h3, #${POPOVER_ID} h3 { margin: 0 0 10px; font-size: 15px; }
#${PANEL_ID} label { display: block; margin: 10px 0 4px; font-weight: 600; }
#${PANEL_ID} input, #${PANEL_ID} textarea {
width: 100%; box-sizing: border-box; border: 1px solid #cbd5e1; border-radius: 8px;
padding: 8px; font: 13px system-ui, sans-serif; background: #fff; color: #0f172a;
}
#${PANEL_ID} textarea { min-height: 88px; resize: vertical; }
#${PANEL_ID} .eks-row, #${POPOVER_ID} .eks-row { display: flex; gap: 8px; align-items: center; }
#${PANEL_ID} .eks-row > * { flex: 1; }
#${PANEL_ID} button, #${POPOVER_ID} button {
border: 0; border-radius: 8px; padding: 8px 10px; background: #1f6feb; color: white;
font: 600 13px system-ui, sans-serif; cursor: pointer;
}
#${PANEL_ID} button.secondary, #${POPOVER_ID} button.secondary { background: #e2e8f0; color: #0f172a; }
#${PANEL_ID} button:disabled, #${POPOVER_ID} button:disabled { opacity: .55; cursor: wait; }
#${PANEL_ID} .eks-muted, #${POPOVER_ID} .eks-muted { color: #64748b; font-size: 12px; }
#${PANEL_ID} .eks-error, #${POPOVER_ID} .eks-error { color: #b91c1c; white-space: pre-wrap; }
#${PANEL_ID} .eks-result, #${POPOVER_ID} .eks-result { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 10px; white-space: pre-wrap; min-height: 44px; }
.eks-inline-translate {
position: absolute !important; top: 4px !important; right: 4px !important; z-index: 6 !important;
border: 0 !important; border-radius: 999px !important; padding: 1px 5px !important;
background: rgba(224,242,254,.94) !important; color: #075985 !important;
font: 700 9px system-ui, sans-serif !important; line-height: 14px !important; cursor: pointer !important;
opacity: .72 !important; box-shadow: 0 1px 4px rgba(15,23,42,.18) !important;
}
.eks-inline-translate:hover { opacity: 1 !important; }
.eks-composer-translate {
border: 0 !important; border-radius: 999px !important; padding: 3px 7px !important;
background: #1f6feb !important; color: white !important; font: 600 11px system-ui, sans-serif !important;
cursor: pointer !important; margin: 0 4px !important; box-shadow: 0 2px 8px rgba(15,23,42,.16) !important;
display: inline-flex !important; align-items: center !important; min-height: 22px !important;
}
.eks-composer-status { display: inline-block; margin-left: 6px; color: #64748b; font: 12px system-ui, sans-serif; }
`;
document.head.appendChild(style);
}
function makeUi() {
ensureStyles();
if (!document.getElementById(BUTTON_ID)) {
const toggle = document.createElement('button');
toggle.id = BUTTON_ID;
toggle.type = 'button';
toggle.textContent = 'AI tłumacz';
toggle.addEventListener('click', () => {
const panel = document.getElementById(PANEL_ID);
panel.hidden = !panel.hidden;
if (!panel.hidden) fillSelectedText();
});
document.body.appendChild(toggle);
}
if (!document.getElementById(POPOVER_ID)) {
const pop = document.createElement('section');
pop.id = POPOVER_ID;
pop.hidden = true;
pop.innerHTML = `
Tłumaczenie wiadomości na polski
`;
pop.querySelector('[data-eks-pop="close"]').addEventListener('click', () => { pop.hidden = true; });
document.body.appendChild(pop);
}
if (document.getElementById(PANEL_ID)) return;
const panel = document.createElement('section');
panel.id = PANEL_ID;
panel.hidden = true;
panel.innerHTML = `
EKS AI tłumaczenia
Token operatora jest zapisany tylko w tej przeglądarce. Sekrety Flowise/OpenAI zostają po stronie EKSRelay.
`;
document.body.appendChild(panel);
const token = panel.querySelector('[data-eks="token"]');
token.value = localStorage.getItem(STORE_KEY) || '';
token.addEventListener('change', () => localStorage.setItem(STORE_KEY, token.value.trim()));
panel.querySelector('[data-eks="fill-selected"]').addEventListener('click', fillSelectedText);
panel.querySelector('[data-eks="selected"]').addEventListener('click', () => translateMessageFromPanel());
panel.querySelector('[data-eks="draft"]').addEventListener('click', () => translateAndReplaceDraft());
panel.querySelector('[data-eks="copy"]').addEventListener('click', copyResult);
panel.querySelector('[data-eks="close"]').addEventListener('click', () => { panel.hidden = true; });
}
function panelField(name) { return document.querySelector(`#${PANEL_ID} [data-eks="${name}"]`); }
function popField(name) { return document.querySelector(`#${POPOVER_ID} [data-eks-pop="${name}"]`); }
function fillSelectedText() {
const selected = String(window.getSelection?.() || '').trim();
if (selected) panelField('text').value = selected;
else {
const draft = getDraftText();
if (draft) panelField('text').value = draft;
}
}
function getDraftText() {
const el = getReplyEditorElement();
return el ? readEditableText(el) : '';
}
function readEditableText(el) {
return el ? (el.value || el.innerText || el.textContent || '') : '';
}
function getReplyEditorElement() {
const active = document.activeElement;
if (active && isReplyEditor(active)) return active;
const explicit = document.querySelector('.reply-box__top .ProseMirror[contenteditable="true"], .reply-box__top [contenteditable="true"].ProseMirror, .reply-box__top [contenteditable="true"][translate="no"]');
if (explicit) return explicit;
const replyBox = document.querySelector('.reply-box__top');
if (replyBox) {
const editor = replyBox.querySelector('.ProseMirror[contenteditable="true"], [contenteditable="true"][role="textbox"], [contenteditable="true"]');
if (editor) return editor;
}
const editors = [...document.querySelectorAll('.ProseMirror[contenteditable="true"], [contenteditable="true"][role="textbox"]')];
return editors.find((el) => !el.closest(`#${PANEL_ID}, #${POPOVER_ID}`) && isLikelyReplyEditor(el)) || null;
}
function isReplyEditor(el) {
return Boolean(el && !el.closest(`#${PANEL_ID}, #${POPOVER_ID}`) && el.closest('.reply-box__top') && (el.isContentEditable || el.classList?.contains('ProseMirror')));
}
function isLikelyReplyEditor(el) {
return Boolean(el && !el.closest('.message, .message-bubble, [data-message-id]') && (el.classList?.contains('ProseMirror') || el.isContentEditable));
}
function getDraftElement() {
const replyEditor = getReplyEditorElement();
if (replyEditor) return replyEditor;
const active = document.activeElement;
if (active && (active.tagName === 'TEXTAREA' || active.isContentEditable) && !active.closest(`#${PANEL_ID}, #${POPOVER_ID}`)) return active;
const editors = [...document.querySelectorAll('.ProseMirror[contenteditable="true"], [contenteditable="true"][role="textbox"], textarea')];
return editors.find((el) => !el.closest(`#${PANEL_ID}`)) || null;
}
function setDraftText(el, text) {
if (!el) return false;
if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
el.value = text;
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
el.focus();
return true;
}
if (el.isContentEditable) {
el.focus();
replaceContentEditableText(el, text);
return true;
}
return false;
}
function replaceContentEditableText(el, text) {
el.focus();
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(el);
selection.removeAllRanges();
selection.addRange(range);
let inserted = false;
try {
inserted = document.execCommand && document.execCommand('insertText', false, text);
} catch (_) {
inserted = false;
}
if (!inserted) {
const normalised = text.split(/\n{2,}/).map((block) => block.trim()).filter(Boolean);
el.innerHTML = normalised.length
? normalised.map((block) => `${escapeHtml(block).replace(/\n/g, '
')}
`).join('')
: '';
el.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, inputType: 'insertText', data: text }));
el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
}
el.dispatchEvent(new Event('change', { bubbles: true }));
}
function escapeHtml(text) {
return text.replace(/[&<>"]/g, (ch) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[ch]));
}
async function translateAndReplaceDraft(button) {
const el = getReplyEditorElement();
const text = readEditableText(el).trim();
const token = getToken();
if (!token) {
showPanelError('Brak Agent UI token. Otwórz raz panel „AI tłumacz” i wklej token.');
return showComposerStatus(button, 'Brak tokenu — otwórz panel AI tłumacz.', true);
}
if (!el) return showComposerStatus(button, 'Nie widzę okna odpowiedzi Chatwoot.', true);
if (!text) return showComposerStatus(button, 'Najpierw wpisz tekst po polsku w oknie odpowiedzi.', true);
const panel = document.getElementById(PANEL_ID);
if (!panel.hidden) {
panelField('source').value = 'pl';
panelField('target').value = 'auto';
panelField('text').value = text;
panelField('status').textContent = 'Wykrywam język klienta z pierwszej wiadomości i tłumaczę tekst z okna odpowiedzi…';
}
showComposerStatus(button, 'Tłumaczę…');
const originalButtonText = button?.textContent || '';
if (button) button.textContent = 'AI…';
try {
setBusy(true);
const translated = await callTranslate('/agent-tools/translate-draft', {
draftText: text,
sourceLanguage: 'pl',
targetLanguage: 'auto',
conversationId: detectConversationId(),
});
state.result = translated.translatedText || '';
if (!panel.hidden) {
panelField('result').textContent = state.result + (translated.cached ? '\n\n[cached]' : '');
panelField('status').textContent = `Gotowe — wstawiłem tłumaczenie do pola odpowiedzi (${translated.targetLanguage}).`;
}
setDraftText(el, state.result);
showComposerStatus(button, `Gotowe (${translated.targetLanguage})`);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
showPanelError(msg);
showComposerStatus(button, msg, true);
} finally {
if (button) button.textContent = originalButtonText;
setBusy(false);
setTimeout(() => showComposerStatus(button, ''), 3500);
}
}
async function translateMessageFromPanel() {
const text = panelField('text').value.trim();
if (!text) return showPanelError('Brak tekstu do tłumaczenia.');
return translateMessageText(text, detectMessageId());
}
async function translateMessageText(text, messageId, button) {
const token = getToken();
if (!token) return showPanelError('Brak Agent UI token.');
showPopover('Tłumaczę wiadomość na polski…', '', '');
if (button) button.textContent = '…';
try {
setBusy(true);
const translated = await callTranslate('/agent-tools/translate-message', {
sourceText: text,
targetLanguage: 'pl',
conversationId: detectConversationId(),
messageId,
});
state.result = translated.translatedText || '';
showPopover(translated.cached ? 'Gotowe (cache).' : 'Gotowe.', state.result, '');
return state.result;
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
showPopover('', '', msg);
} finally {
if (button) button.textContent = 'AI';
setBusy(false);
}
}
async function callTranslate(endpoint, payload) {
const token = getToken();
localStorage.setItem(STORE_KEY, token);
const res = await fetch(`${RELAY_ORIGIN}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify(payload),
});
const json = await res.json().catch(() => null);
if (!res.ok || !json?.ok) throw new Error(json?.message || `HTTP ${res.status}`);
return json;
}
function getToken() {
return panelField('token')?.value.trim() || localStorage.getItem(STORE_KEY) || '';
}
function detectConversationId() {
const m = location.pathname.match(/conversations\/(\d+)/) || location.hash.match(/conversations\/(\d+)/);
return m ? Number(m[1]) : undefined;
}
function detectMessageId() {
const el = window.getSelection?.()?.anchorNode?.parentElement?.closest?.('[data-message-id], [data-id]');
return el?.getAttribute('data-message-id') || el?.getAttribute('data-id') || undefined;
}
async function copyResult() {
if (!state.result) return;
await navigator.clipboard.writeText(state.result);
}
function showPanelError(msg) {
state.error = msg;
const el = panelField('error');
if (el) el.textContent = msg;
}
function showPopover(status, result, error) {
const pop = document.getElementById(POPOVER_ID);
pop.hidden = false;
popField('status').textContent = status || '';
popField('result').textContent = result || '';
popField('error').textContent = error || '';
}
function showComposerStatus(button, msg, isError = false) {
const host = button?.parentElement;
let el = host?.querySelector?.('.eks-composer-status');
if (!el && host) {
el = document.createElement('span');
el.className = 'eks-composer-status';
host.appendChild(el);
}
if (el) {
el.textContent = msg || '';
el.style.color = isError ? '#b91c1c' : '#64748b';
}
}
function setBusy(busy) {
state.busy = busy;
document.querySelectorAll(`#${PANEL_ID} button, #${POPOVER_ID} button, .eks-composer-translate, .eks-inline-translate`).forEach((btn) => { btn.disabled = busy; });
}
function addInlineButtons() {
const candidates = document.querySelectorAll('[data-message-id]:not([data-eks-ai-bound]), .message-bubble:not([data-eks-ai-bound]), [class*="message"]:not([data-eks-ai-bound])');
candidates.forEach((el) => {
const text = (el.innerText || '').trim();
if (text.length < 12 || text.length > 5000) return;
if (el.closest(`#${PANEL_ID}, #${POPOVER_ID}`)) return;
el.setAttribute('data-eks-ai-bound', '1');
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'eks-inline-translate';
btn.textContent = 'AI';
btn.title = 'Przetłumacz tę wiadomość na polski';
btn.addEventListener('click', (e) => {
e.preventDefault(); e.stopPropagation();
void translateMessageText(text, el.getAttribute('data-message-id') || el.getAttribute('data-id') || undefined, btn);
});
el.appendChild(btn);
});
}
function addComposerButtons() {
const editors = document.querySelectorAll('.reply-box__top .ProseMirror[contenteditable="true"], .reply-box__top [contenteditable="true"].ProseMirror, .reply-box__top [contenteditable="true"][translate="no"]');
editors.forEach((el) => {
if (el.dataset?.eksComposerBound === '1') return;
if (el.closest(`#${PANEL_ID}, #${POPOVER_ID}`)) return;
if (el.dataset) el.dataset.eksComposerBound = '1';
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'eks-composer-translate';
btn.textContent = 'PL→klient';
btn.title = 'Przetłumacz wpisaną odpowiedź po polsku na język wykryty z pierwszej wiadomości klienta';
btn.addEventListener('click', (e) => {
e.preventDefault(); e.stopPropagation();
void translateAndReplaceDraft(btn);
});
const menubar = el.closest('.ProseMirror-menubar-wrapper')?.querySelector('.ProseMirror-menubar');
const host = menubar || el.parentElement;
if (host) host.appendChild(btn);
});
}
function boot() {
if (!document.body) return setTimeout(boot, 200);
makeUi();
addInlineButtons();
addComposerButtons();
const obs = new MutationObserver(() => { addInlineButtons(); addComposerButtons(); });
obs.observe(document.body, { childList: true, subtree: true });
}
boot();
})();