|
|
@@ -0,0 +1,247 @@
|
|
|
+(() => {
|
|
|
+ '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 LANG_KEY = 'eksAgentUiTargetLanguage';
|
|
|
+ const PANEL_ID = 'eks-ai-translate-panel';
|
|
|
+ const BUTTON_ID = 'eks-ai-translate-toggle';
|
|
|
+
|
|
|
+ const state = {
|
|
|
+ busy: false,
|
|
|
+ result: '',
|
|
|
+ error: '',
|
|
|
+ };
|
|
|
+
|
|
|
+ function t(s) { return s; }
|
|
|
+
|
|
|
+ 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} {
|
|
|
+ position: fixed; right: 18px; bottom: 66px; z-index: 2147483000;
|
|
|
+ width: min(420px, calc(100vw - 36px)); max-height: min(720px, calc(100vh - 92px));
|
|
|
+ overflow: auto; background: #fff; color: #0f172a; border: 1px solid #d7dee8;
|
|
|
+ border-radius: 14px; box-shadow: 0 18px 48px rgba(15,23,42,.28);
|
|
|
+ padding: 14px; font: 13px system-ui, sans-serif;
|
|
|
+ }
|
|
|
+ #${PANEL_ID}[hidden] { display: none; }
|
|
|
+ #${PANEL_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, #${PANEL_ID} select {
|
|
|
+ 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 { display: flex; gap: 8px; align-items: center; }
|
|
|
+ #${PANEL_ID} .eks-row > * { flex: 1; }
|
|
|
+ #${PANEL_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 { background: #e2e8f0; color: #0f172a; }
|
|
|
+ #${PANEL_ID} button.danger { background: #b91c1c; }
|
|
|
+ #${PANEL_ID} button:disabled { opacity: .55; cursor: wait; }
|
|
|
+ #${PANEL_ID} .eks-muted { color: #64748b; font-size: 12px; }
|
|
|
+ #${PANEL_ID} .eks-error { color: #b91c1c; white-space: pre-wrap; }
|
|
|
+ #${PANEL_ID} .eks-result { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 10px; white-space: pre-wrap; min-height: 44px; }
|
|
|
+ .eks-inline-translate {
|
|
|
+ margin-left: 6px !important; border: 0 !important; border-radius: 999px !important;
|
|
|
+ padding: 3px 7px !important; background: #e0f2fe !important; color: #075985 !important;
|
|
|
+ font: 600 11px system-ui, sans-serif !important; cursor: pointer !important;
|
|
|
+ }
|
|
|
+ `;
|
|
|
+ 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(PANEL_ID)) return;
|
|
|
+ const panel = document.createElement('section');
|
|
|
+ panel.id = PANEL_ID;
|
|
|
+ panel.hidden = true;
|
|
|
+ panel.innerHTML = `
|
|
|
+ <h3>${t('EKS AI tłumaczenia')}</h3>
|
|
|
+ <div class="eks-muted">Token operatora jest zapisany tylko w tej przeglądarce. OpenAI/Flowise sekrety 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</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ść</button>
|
|
|
+ <button data-eks="draft" type="button">Tłumacz draft</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>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"]');
|
|
|
+ const target = panel.querySelector('[data-eks="target"]');
|
|
|
+ token.value = localStorage.getItem(STORE_KEY) || '';
|
|
|
+ target.value = localStorage.getItem(LANG_KEY) || 'pl';
|
|
|
+ token.addEventListener('change', () => localStorage.setItem(STORE_KEY, token.value.trim()));
|
|
|
+ target.addEventListener('change', () => localStorage.setItem(LANG_KEY, target.value.trim() || 'pl'));
|
|
|
+ panel.querySelector('[data-eks="fill-selected"]').addEventListener('click', fillSelectedText);
|
|
|
+ panel.querySelector('[data-eks="selected"]').addEventListener('click', () => translate('message'));
|
|
|
+ panel.querySelector('[data-eks="draft"]').addEventListener('click', () => translate('draft'));
|
|
|
+ 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 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 active = document.activeElement;
|
|
|
+ if (active && (active.tagName === 'TEXTAREA' || active.tagName === 'INPUT')) return active.value || '';
|
|
|
+ if (active?.isContentEditable) return active.innerText || active.textContent || '';
|
|
|
+ const candidate = document.querySelector('[contenteditable="true"][role="textbox"], textarea');
|
|
|
+ return candidate ? (candidate.value || candidate.innerText || candidate.textContent || '') : '';
|
|
|
+ }
|
|
|
+
|
|
|
+ async function translate(mode) {
|
|
|
+ const token = panelField('token').value.trim();
|
|
|
+ const targetLanguage = panelField('target').value.trim() || 'pl';
|
|
|
+ const sourceLanguageRaw = panelField('source').value.trim();
|
|
|
+ let text = panelField('text').value.trim();
|
|
|
+ if (mode === 'draft' && !text) text = getDraftText().trim();
|
|
|
+ if (!token) return showError('Brak Agent UI token.');
|
|
|
+ if (!text) return showError('Brak tekstu do tłumaczenia.');
|
|
|
+ localStorage.setItem(STORE_KEY, token);
|
|
|
+ localStorage.setItem(LANG_KEY, targetLanguage);
|
|
|
+
|
|
|
+ setBusy(true);
|
|
|
+ showError('');
|
|
|
+ panelField('result').textContent = '';
|
|
|
+ try {
|
|
|
+ const endpoint = mode === 'draft' ? '/agent-tools/translate-draft' : '/agent-tools/translate-message';
|
|
|
+ const payload = {
|
|
|
+ targetLanguage,
|
|
|
+ sourceLanguage: sourceLanguageRaw && sourceLanguageRaw !== 'auto' ? sourceLanguageRaw : undefined,
|
|
|
+ conversationId: detectConversationId(),
|
|
|
+ ...(mode === 'draft' ? { draftText: text } : { sourceText: text, messageId: detectMessageId() }),
|
|
|
+ };
|
|
|
+ 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}`);
|
|
|
+ state.result = json.translatedText || '';
|
|
|
+ panelField('result').textContent = state.result + (json.cached ? '\n\n[cached]' : '');
|
|
|
+ } catch (err) {
|
|
|
+ showError(err instanceof Error ? err.message : String(err));
|
|
|
+ } finally {
|
|
|
+ setBusy(false);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 showError(msg) {
|
|
|
+ state.error = msg;
|
|
|
+ const el = panelField('error');
|
|
|
+ if (el) el.textContent = msg;
|
|
|
+ }
|
|
|
+
|
|
|
+ function setBusy(busy) {
|
|
|
+ state.busy = busy;
|
|
|
+ document.querySelectorAll(`#${PANEL_ID} button`).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;
|
|
|
+ el.setAttribute('data-eks-ai-bound', '1');
|
|
|
+ const btn = document.createElement('button');
|
|
|
+ btn.type = 'button';
|
|
|
+ btn.className = 'eks-inline-translate';
|
|
|
+ btn.textContent = 'AI→PL';
|
|
|
+ btn.addEventListener('click', (e) => {
|
|
|
+ e.preventDefault(); e.stopPropagation();
|
|
|
+ const panel = document.getElementById(PANEL_ID);
|
|
|
+ panel.hidden = false;
|
|
|
+ panelField('target').value = localStorage.getItem(LANG_KEY) || 'pl';
|
|
|
+ panelField('text').value = text;
|
|
|
+ });
|
|
|
+ el.appendChild(btn);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function boot() {
|
|
|
+ if (!document.body) return setTimeout(boot, 200);
|
|
|
+ makeUi();
|
|
|
+ addInlineButtons();
|
|
|
+ const obs = new MutationObserver(() => addInlineButtons());
|
|
|
+ obs.observe(document.body, { childList: true, subtree: true });
|
|
|
+ }
|
|
|
+
|
|
|
+ boot();
|
|
|
+})();
|