| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373 |
- (() => {
- '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: 5px 9px !important;
- background: #1f6feb !important; color: white !important; font: 600 12px system-ui, sans-serif !important;
- cursor: pointer !important; margin: 4px !important; box-shadow: 0 2px 8px rgba(15,23,42,.16) !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 = `
- <div class="eks-row" style="justify-content:space-between">
- <h3>Tłumaczenie wiadomości na polski</h3>
- <button data-eks-pop="close" class="secondary" type="button">Zamknij</button>
- </div>
- <div data-eks-pop="status" class="eks-muted"></div>
- <div data-eks-pop="result" class="eks-result"></div>
- <div data-eks-pop="error" class="eks-error"></div>
- `;
- 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 = `
- <h3>EKS AI tłumaczenia</h3>
- <div class="eks-muted">Token operatora jest zapisany tylko w tej przeglądarce. Sekrety Flowise/OpenAI zostają po stronie EKSRelay.</div>
- <label>Agent UI token</label>
- <input data-eks="token" type="password" autocomplete="off" placeholder="Wklej token operatora" />
- <div class="eks-row">
- <div><label>Język docelowy dla wiadomości</label><input data-eks="target" value="pl" placeholder="pl, en, de, cs..." /></div>
- <div><label>Źródłowy</label><input data-eks="source" value="auto" placeholder="auto" /></div>
- </div>
- <label>Tekst do tłumaczenia</label>
- <textarea data-eks="text" placeholder="Zaznacz tekst wiadomości albo wklej go tutaj"></textarea>
- <div class="eks-row" style="margin-top:8px">
- <button data-eks="selected" type="button">Tłumacz wiadomość na PL</button>
- <button data-eks="draft" type="button">Draft PL → język klienta</button>
- </div>
- <div class="eks-row" style="margin-top:8px">
- <button data-eks="fill-selected" class="secondary" type="button">Wstaw zaznaczenie</button>
- <button data-eks="copy" class="secondary" type="button">Kopiuj wynik</button>
- <button data-eks="close" class="secondary" type="button">Zamknij</button>
- </div>
- <label>Status</label>
- <div data-eks="status" class="eks-muted"></div>
- <label>Wynik</label>
- <div data-eks="result" class="eks-result"></div>
- <div data-eks="error" class="eks-error"></div>
- `;
- 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 = getDraftElement();
- return el ? (el.value || el.innerText || el.textContent || '') : '';
- }
- function getDraftElement() {
- const active = document.activeElement;
- if (active && (active.tagName === 'TEXTAREA' || active.tagName === 'INPUT' || active.isContentEditable)) return active;
- const editors = [...document.querySelectorAll('[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();
- el.textContent = text;
- el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
- return true;
- }
- return false;
- }
- async function translateAndReplaceDraft(button) {
- const el = getDraftElement();
- const text = (el ? (el.value || el.innerText || el.textContent || '') : '').trim();
- const token = getToken();
- if (!token) return showPanelError('Brak Agent UI token.');
- if (!text) return showComposerStatus(button, 'Najpierw wpisz odpowiedź po polsku.', true);
- const panel = document.getElementById(PANEL_ID);
- panel.hidden = false;
- panelField('source').value = 'pl';
- panelField('text').value = text;
- panelField('status').textContent = 'Wykrywam język klienta z pierwszej wiadomości i tłumaczę draft…';
- showComposerStatus(button, 'Tłumaczę…');
- try {
- setBusy(true);
- const translated = await callTranslate('/agent-tools/translate-draft', {
- draftText: text,
- sourceLanguage: 'pl',
- targetLanguage: 'auto',
- conversationId: detectConversationId(),
- });
- state.result = translated.translatedText || '';
- panelField('result').textContent = state.result + (translated.cached ? '\n\n[cached]' : '');
- panelField('status').textContent = 'Gotowe — wstawiłem tłumaczenie do pola odpowiedzi.';
- setDraftText(el, state.result);
- showComposerStatus(button, 'Gotowe');
- } catch (err) {
- const msg = err instanceof Error ? err.message : String(err);
- showPanelError(msg);
- showComposerStatus(button, msg, true);
- } finally {
- 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('[contenteditable="true"][role="textbox"], textarea');
- 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 = 'AI: PL → język klienta';
- 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 host = 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();
- })();
|