|
@@ -6,17 +6,11 @@
|
|
|
|
|
|
|
|
const RELAY_ORIGIN = new URL(document.currentScript?.src || 'https://eks-relay.easyklima.com/chatwoot-ai-tools.js').origin;
|
|
const RELAY_ORIGIN = new URL(document.currentScript?.src || 'https://eks-relay.easyklima.com/chatwoot-ai-tools.js').origin;
|
|
|
const STORE_KEY = 'eksAgentUiToken';
|
|
const STORE_KEY = 'eksAgentUiToken';
|
|
|
- const LANG_KEY = 'eksAgentUiTargetLanguage';
|
|
|
|
|
const PANEL_ID = 'eks-ai-translate-panel';
|
|
const PANEL_ID = 'eks-ai-translate-panel';
|
|
|
const BUTTON_ID = 'eks-ai-translate-toggle';
|
|
const BUTTON_ID = 'eks-ai-translate-toggle';
|
|
|
|
|
+ const POPOVER_ID = 'eks-ai-translate-popover';
|
|
|
|
|
|
|
|
- const state = {
|
|
|
|
|
- busy: false,
|
|
|
|
|
- result: '',
|
|
|
|
|
- error: '',
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- function t(s) { return s; }
|
|
|
|
|
|
|
+ const state = { busy: false, result: '', error: '' };
|
|
|
|
|
|
|
|
function ensureStyles() {
|
|
function ensureStyles() {
|
|
|
if (document.getElementById('eks-ai-translate-styles')) return;
|
|
if (document.getElementById('eks-ai-translate-styles')) return;
|
|
@@ -29,44 +23,49 @@
|
|
|
background: #1f6feb; color: white; font: 600 13px system-ui, sans-serif;
|
|
background: #1f6feb; color: white; font: 600 13px system-ui, sans-serif;
|
|
|
box-shadow: 0 8px 22px rgba(15,23,42,.25); cursor: pointer;
|
|
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} {
|
|
#${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;
|
|
|
|
|
|
|
+ right: 18px; bottom: 66px; width: min(420px, calc(100vw - 36px));
|
|
|
|
|
+ max-height: min(720px, calc(100vh - 92px)); overflow: auto; padding: 14px;
|
|
|
}
|
|
}
|
|
|
- #${PANEL_ID}[hidden] { display: none; }
|
|
|
|
|
- #${PANEL_ID} h3 { margin: 0 0 10px; font-size: 15px; }
|
|
|
|
|
|
|
+ #${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} label { display: block; margin: 10px 0 4px; font-weight: 600; }
|
|
|
- #${PANEL_ID} input, #${PANEL_ID} textarea, #${PANEL_ID} select {
|
|
|
|
|
|
|
+ #${PANEL_ID} input, #${PANEL_ID} textarea {
|
|
|
width: 100%; box-sizing: border-box; border: 1px solid #cbd5e1; border-radius: 8px;
|
|
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;
|
|
padding: 8px; font: 13px system-ui, sans-serif; background: #fff; color: #0f172a;
|
|
|
}
|
|
}
|
|
|
#${PANEL_ID} textarea { min-height: 88px; resize: vertical; }
|
|
#${PANEL_ID} textarea { min-height: 88px; resize: vertical; }
|
|
|
- #${PANEL_ID} .eks-row { display: flex; gap: 8px; align-items: center; }
|
|
|
|
|
|
|
+ #${PANEL_ID} .eks-row, #${POPOVER_ID} .eks-row { display: flex; gap: 8px; align-items: center; }
|
|
|
#${PANEL_ID} .eks-row > * { flex: 1; }
|
|
#${PANEL_ID} .eks-row > * { flex: 1; }
|
|
|
- #${PANEL_ID} button {
|
|
|
|
|
|
|
+ #${PANEL_ID} button, #${POPOVER_ID} button {
|
|
|
border: 0; border-radius: 8px; padding: 8px 10px; background: #1f6feb; color: white;
|
|
border: 0; border-radius: 8px; padding: 8px 10px; background: #1f6feb; color: white;
|
|
|
font: 600 13px system-ui, sans-serif; cursor: pointer;
|
|
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; }
|
|
|
|
|
|
|
+ #${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 {
|
|
.eks-inline-translate {
|
|
|
- margin-left: 4px !important; border: 0 !important; border-radius: 999px !important;
|
|
|
|
|
- padding: 1px 5px !important; background: #e0f2fe !important; color: #075985 !important;
|
|
|
|
|
- font: 600 9px system-ui, sans-serif !important; line-height: 14px !important; cursor: pointer !important;
|
|
|
|
|
- opacity: .82 !important; vertical-align: middle !important;
|
|
|
|
|
|
|
+ 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 {
|
|
.eks-composer-translate {
|
|
|
border: 0 !important; border-radius: 999px !important; padding: 5px 9px !important;
|
|
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;
|
|
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;
|
|
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);
|
|
document.head.appendChild(style);
|
|
|
}
|
|
}
|
|
@@ -86,36 +85,49 @@
|
|
|
document.body.appendChild(toggle);
|
|
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;
|
|
if (document.getElementById(PANEL_ID)) return;
|
|
|
const panel = document.createElement('section');
|
|
const panel = document.createElement('section');
|
|
|
panel.id = PANEL_ID;
|
|
panel.id = PANEL_ID;
|
|
|
panel.hidden = true;
|
|
panel.hidden = true;
|
|
|
panel.innerHTML = `
|
|
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>
|
|
|
|
|
|
|
+ <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>
|
|
<label>Agent UI token</label>
|
|
|
<input data-eks="token" type="password" autocomplete="off" placeholder="Wklej token operatora" />
|
|
<input data-eks="token" type="password" autocomplete="off" placeholder="Wklej token operatora" />
|
|
|
<div class="eks-row">
|
|
<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>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>
|
|
</div>
|
|
|
<label>Tekst do tłumaczenia</label>
|
|
<label>Tekst do tłumaczenia</label>
|
|
|
<textarea data-eks="text" placeholder="Zaznacz tekst wiadomości albo wklej go tutaj"></textarea>
|
|
<textarea data-eks="text" placeholder="Zaznacz tekst wiadomości albo wklej go tutaj"></textarea>
|
|
|
<div class="eks-row" style="margin-top:8px">
|
|
<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>
|
|
|
|
|
|
|
+ <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>
|
|
|
<div class="eks-row" style="margin-top:8px">
|
|
<div class="eks-row" style="margin-top:8px">
|
|
|
<button data-eks="fill-selected" class="secondary" type="button">Wstaw zaznaczenie</button>
|
|
<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="copy" class="secondary" type="button">Kopiuj wynik</button>
|
|
|
<button data-eks="close" class="secondary" type="button">Zamknij</button>
|
|
<button data-eks="close" class="secondary" type="button">Zamknij</button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <label>Status</label>
|
|
|
|
|
+ <div data-eks="status" class="eks-muted"></div>
|
|
|
<label>Wynik</label>
|
|
<label>Wynik</label>
|
|
|
<div data-eks="result" class="eks-result"></div>
|
|
<div data-eks="result" class="eks-result"></div>
|
|
|
<div data-eks="error" class="eks-error"></div>
|
|
<div data-eks="error" class="eks-error"></div>
|
|
@@ -123,21 +135,17 @@
|
|
|
document.body.appendChild(panel);
|
|
document.body.appendChild(panel);
|
|
|
|
|
|
|
|
const token = panel.querySelector('[data-eks="token"]');
|
|
const token = panel.querySelector('[data-eks="token"]');
|
|
|
- const target = panel.querySelector('[data-eks="target"]');
|
|
|
|
|
token.value = localStorage.getItem(STORE_KEY) || '';
|
|
token.value = localStorage.getItem(STORE_KEY) || '';
|
|
|
- target.value = localStorage.getItem(LANG_KEY) || 'pl';
|
|
|
|
|
token.addEventListener('change', () => localStorage.setItem(STORE_KEY, token.value.trim()));
|
|
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="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="selected"]').addEventListener('click', () => translateMessageFromPanel());
|
|
|
|
|
+ panel.querySelector('[data-eks="draft"]').addEventListener('click', () => translateAndReplaceDraft());
|
|
|
panel.querySelector('[data-eks="copy"]').addEventListener('click', copyResult);
|
|
panel.querySelector('[data-eks="copy"]').addEventListener('click', copyResult);
|
|
|
panel.querySelector('[data-eks="close"]').addEventListener('click', () => { panel.hidden = true; });
|
|
panel.querySelector('[data-eks="close"]').addEventListener('click', () => { panel.hidden = true; });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- function panelField(name) {
|
|
|
|
|
- return document.querySelector(`#${PANEL_ID} [data-eks="${name}"]`);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ 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() {
|
|
function fillSelectedText() {
|
|
|
const selected = String(window.getSelection?.() || '').trim();
|
|
const selected = String(window.getSelection?.() || '').trim();
|
|
@@ -149,17 +157,15 @@
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function getDraftText() {
|
|
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 || '') : '';
|
|
|
|
|
|
|
+ const el = getDraftElement();
|
|
|
|
|
+ return el ? (el.value || el.innerText || el.textContent || '') : '';
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function getDraftElement() {
|
|
function getDraftElement() {
|
|
|
const active = document.activeElement;
|
|
const active = document.activeElement;
|
|
|
if (active && (active.tagName === 'TEXTAREA' || active.tagName === 'INPUT' || active.isContentEditable)) return active;
|
|
if (active && (active.tagName === 'TEXTAREA' || active.tagName === 'INPUT' || active.isContentEditable)) return active;
|
|
|
- return document.querySelector('[contenteditable="true"][role="textbox"], textarea');
|
|
|
|
|
|
|
+ const editors = [...document.querySelectorAll('[contenteditable="true"][role="textbox"], textarea')];
|
|
|
|
|
+ return editors.find((el) => !el.closest(`#${PANEL_ID}`)) || null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function setDraftText(el, text) {
|
|
function setDraftText(el, text) {
|
|
@@ -180,59 +186,91 @@
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- async function translateAndReplaceDraft() {
|
|
|
|
|
|
|
+ async function translateAndReplaceDraft(button) {
|
|
|
const el = getDraftElement();
|
|
const el = getDraftElement();
|
|
|
const text = (el ? (el.value || el.innerText || el.textContent || '') : '').trim();
|
|
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);
|
|
const panel = document.getElementById(PANEL_ID);
|
|
|
panel.hidden = false;
|
|
panel.hidden = false;
|
|
|
panelField('source').value = 'pl';
|
|
panelField('source').value = 'pl';
|
|
|
panelField('text').value = text;
|
|
panelField('text').value = text;
|
|
|
- if (!panelField('target').value.trim() || panelField('target').value.trim() === 'pl') {
|
|
|
|
|
- panelField('target').value = localStorage.getItem(LANG_KEY) || 'auto';
|
|
|
|
|
|
|
+ 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);
|
|
|
}
|
|
}
|
|
|
- const translated = await translate('draft');
|
|
|
|
|
- if (translated) setDraftText(el, translated);
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- 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);
|
|
|
|
|
|
|
+ async function translateMessageFromPanel() {
|
|
|
|
|
+ const text = panelField('text').value.trim();
|
|
|
|
|
+ if (!text) return showPanelError('Brak tekstu do tłumaczenia.');
|
|
|
|
|
+ return translateMessageText(text, detectMessageId());
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- setBusy(true);
|
|
|
|
|
- showError('');
|
|
|
|
|
- panelField('result').textContent = '';
|
|
|
|
|
|
|
+ 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 {
|
|
try {
|
|
|
- const endpoint = mode === 'draft' ? '/agent-tools/translate-draft' : '/agent-tools/translate-message';
|
|
|
|
|
- const payload = {
|
|
|
|
|
- targetLanguage,
|
|
|
|
|
- sourceLanguage: sourceLanguageRaw && sourceLanguageRaw !== 'auto' ? sourceLanguageRaw : undefined,
|
|
|
|
|
|
|
+ setBusy(true);
|
|
|
|
|
+ const translated = await callTranslate('/agent-tools/translate-message', {
|
|
|
|
|
+ sourceText: text,
|
|
|
|
|
+ targetLanguage: 'pl',
|
|
|
conversationId: detectConversationId(),
|
|
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),
|
|
|
|
|
|
|
+ messageId,
|
|
|
});
|
|
});
|
|
|
- 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]' : '');
|
|
|
|
|
|
|
+ state.result = translated.translatedText || '';
|
|
|
|
|
+ showPopover(translated.cached ? 'Gotowe (cache).' : 'Gotowe.', state.result, '');
|
|
|
return state.result;
|
|
return state.result;
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
- showError(err instanceof Error ? err.message : String(err));
|
|
|
|
|
|
|
+ const msg = err instanceof Error ? err.message : String(err);
|
|
|
|
|
+ showPopover('', '', msg);
|
|
|
} finally {
|
|
} finally {
|
|
|
|
|
+ if (button) button.textContent = 'AI';
|
|
|
setBusy(false);
|
|
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() {
|
|
function detectConversationId() {
|
|
|
const m = location.pathname.match(/conversations\/(\d+)/) || location.hash.match(/conversations\/(\d+)/);
|
|
const m = location.pathname.match(/conversations\/(\d+)/) || location.hash.match(/conversations\/(\d+)/);
|
|
|
return m ? Number(m[1]) : undefined;
|
|
return m ? Number(m[1]) : undefined;
|
|
@@ -248,15 +286,37 @@
|
|
|
await navigator.clipboard.writeText(state.result);
|
|
await navigator.clipboard.writeText(state.result);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- function showError(msg) {
|
|
|
|
|
|
|
+ function showPanelError(msg) {
|
|
|
state.error = msg;
|
|
state.error = msg;
|
|
|
const el = panelField('error');
|
|
const el = panelField('error');
|
|
|
if (el) el.textContent = msg;
|
|
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) {
|
|
function setBusy(busy) {
|
|
|
state.busy = busy;
|
|
state.busy = busy;
|
|
|
- document.querySelectorAll(`#${PANEL_ID} button`).forEach((btn) => { btn.disabled = busy; });
|
|
|
|
|
|
|
+ document.querySelectorAll(`#${PANEL_ID} button, #${POPOVER_ID} button, .eks-composer-translate, .eks-inline-translate`).forEach((btn) => { btn.disabled = busy; });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function addInlineButtons() {
|
|
function addInlineButtons() {
|
|
@@ -264,7 +324,9 @@
|
|
|
candidates.forEach((el) => {
|
|
candidates.forEach((el) => {
|
|
|
const text = (el.innerText || '').trim();
|
|
const text = (el.innerText || '').trim();
|
|
|
if (text.length < 12 || text.length > 5000) return;
|
|
if (text.length < 12 || text.length > 5000) return;
|
|
|
|
|
+ if (el.closest(`#${PANEL_ID}, #${POPOVER_ID}`)) return;
|
|
|
el.setAttribute('data-eks-ai-bound', '1');
|
|
el.setAttribute('data-eks-ai-bound', '1');
|
|
|
|
|
+ if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
|
|
|
const btn = document.createElement('button');
|
|
const btn = document.createElement('button');
|
|
|
btn.type = 'button';
|
|
btn.type = 'button';
|
|
|
btn.className = 'eks-inline-translate';
|
|
btn.className = 'eks-inline-translate';
|
|
@@ -272,10 +334,7 @@
|
|
|
btn.title = 'Przetłumacz tę wiadomość na polski';
|
|
btn.title = 'Przetłumacz tę wiadomość na polski';
|
|
|
btn.addEventListener('click', (e) => {
|
|
btn.addEventListener('click', (e) => {
|
|
|
e.preventDefault(); e.stopPropagation();
|
|
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;
|
|
|
|
|
|
|
+ void translateMessageText(text, el.getAttribute('data-message-id') || el.getAttribute('data-id') || undefined, btn);
|
|
|
});
|
|
});
|
|
|
el.appendChild(btn);
|
|
el.appendChild(btn);
|
|
|
});
|
|
});
|
|
@@ -285,16 +344,16 @@
|
|
|
const editors = document.querySelectorAll('[contenteditable="true"][role="textbox"], textarea');
|
|
const editors = document.querySelectorAll('[contenteditable="true"][role="textbox"], textarea');
|
|
|
editors.forEach((el) => {
|
|
editors.forEach((el) => {
|
|
|
if (el.dataset?.eksComposerBound === '1') return;
|
|
if (el.dataset?.eksComposerBound === '1') return;
|
|
|
- if (el.closest(`#${PANEL_ID}`)) return;
|
|
|
|
|
|
|
+ if (el.closest(`#${PANEL_ID}, #${POPOVER_ID}`)) return;
|
|
|
if (el.dataset) el.dataset.eksComposerBound = '1';
|
|
if (el.dataset) el.dataset.eksComposerBound = '1';
|
|
|
const btn = document.createElement('button');
|
|
const btn = document.createElement('button');
|
|
|
btn.type = 'button';
|
|
btn.type = 'button';
|
|
|
btn.className = 'eks-composer-translate';
|
|
btn.className = 'eks-composer-translate';
|
|
|
btn.textContent = 'AI: PL → język klienta';
|
|
btn.textContent = 'AI: PL → język klienta';
|
|
|
- btn.title = 'Przetłumacz wpisaną odpowiedź po polsku na język klienta i wstaw do edytora';
|
|
|
|
|
|
|
+ btn.title = 'Przetłumacz wpisaną odpowiedź po polsku na język wykryty z pierwszej wiadomości klienta';
|
|
|
btn.addEventListener('click', (e) => {
|
|
btn.addEventListener('click', (e) => {
|
|
|
e.preventDefault(); e.stopPropagation();
|
|
e.preventDefault(); e.stopPropagation();
|
|
|
- void translateAndReplaceDraft();
|
|
|
|
|
|
|
+ void translateAndReplaceDraft(btn);
|
|
|
});
|
|
});
|
|
|
const host = el.parentElement;
|
|
const host = el.parentElement;
|
|
|
if (host) host.appendChild(btn);
|
|
if (host) host.appendChild(btn);
|