소스 검색

Target Chatwoot ProseMirror reply editor

Maciek 3 주 전
부모
커밋
f5a7d74d8d
1개의 변경된 파일79개의 추가작업 그리고 15개의 파일을 삭제
  1. 79 15
      public/chatwoot-ai-tools.js

+ 79 - 15
public/chatwoot-ai-tools.js

@@ -61,9 +61,10 @@
       }
       .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;
+        border: 0 !important; border-radius: 999px !important; padding: 4px 8px !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; }
     `;
@@ -119,7 +120,7 @@
       <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>
+        <button data-eks="draft" type="button">Tekst z okna 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>
@@ -157,14 +158,45 @@
   }
 
   function getDraftText() {
-    const el = getDraftElement();
+    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.tagName === 'INPUT' || active.isContentEditable)) return active;
-    const editors = [...document.querySelectorAll('[contenteditable="true"][role="textbox"], textarea')];
+    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;
   }
 
@@ -179,25 +211,56 @@
     }
     if (el.isContentEditable) {
       el.focus();
-      el.textContent = text;
-      el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
+      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) => `<p>${escapeHtml(block).replace(/\n/g, '<br>')}</p>`).join('')
+        : '<p></p>';
+      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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[ch]));
+  }
+
   async function translateAndReplaceDraft(button) {
-    const el = getDraftElement();
-    const text = (el ? (el.value || el.innerText || el.textContent || '') : '').trim();
+    const el = getReplyEditorElement();
+    const text = readEditableText(el).trim();
     const token = getToken();
     if (!token) return showPanelError('Brak Agent UI token.');
-    if (!text) return showComposerStatus(button, 'Najpierw wpisz odpowiedź po polsku.', 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);
     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…';
+    panelField('status').textContent = 'Wykrywam język klienta z pierwszej wiadomości i tłumaczę tekst z okna odpowiedzi…';
     showComposerStatus(button, 'Tłumaczę…');
 
     try {
@@ -341,7 +404,7 @@
   }
 
   function addComposerButtons() {
-    const editors = document.querySelectorAll('[contenteditable="true"][role="textbox"], textarea');
+    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;
@@ -355,7 +418,8 @@
         e.preventDefault(); e.stopPropagation();
         void translateAndReplaceDraft(btn);
       });
-      const host = el.parentElement;
+      const menubar = el.closest('.ProseMirror-menubar-wrapper')?.querySelector('.ProseMirror-menubar');
+      const host = menubar || el.parentElement;
       if (host) host.appendChild(btn);
     });
   }