(() => { '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(); })();