MapReply Copilot

Minimal WME UR helper rebuilt around direct UR pane integration for templates, translation, and AI replies.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         MapReply Copilot
// @namespace    https://example.local/mapreply-copilot
// @version      1.0.0
// @description  Minimal WME UR helper rebuilt around direct UR pane integration for templates, translation, and AI replies.
// @author       Dutrus
// @match        https://www.waze.com/*/editor*
// @match        https://www.waze.com/editor*
// @match        https://beta.waze.com/*/editor*
// @match        https://beta.waze.com/editor*
// @exclude      https://www.waze.com/*/user/editor/*
// @exclude      https://www.waze.com/discuss/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @connect      translate.googleapis.com
// @connect      api.openai.com
// @run-at       document-idle
// ==/UserScript==

(function () {
  "use strict";

  const SCRIPT_ID = "mapreply-copilot";
  const ROOT_ID = `${SCRIPT_ID}-root`;
  const STYLE_ID = `${SCRIPT_ID}-styles`;
  const DEFAULT_SETTINGS = {
    selectedTemplate: "clarify",
    editorLanguage: "",
    openAiApiKey: "",
    aiModel: "gpt-4.1-mini"
  };

  const TEMPLATE_LIBRARY = {
    clarify: {
      label: "Clarify",
      text: "Thanks for your report. Could you please share a little more detail so we can confirm the issue and update the map correctly?"
    },
    review: {
      label: "Review",
      text: "Thanks for reporting this. We are reviewing the reported issue and will update the map if needed."
    },
    resolved: {
      label: "Resolved",
      text: "Thanks for your report. The issue appears to be corrected now. Please let us know if you still experience the same problem."
    },
    guidance: {
      label: "Guidance",
      text: "Thanks for the report. A route example, nearby landmark, or exact direction of travel would help us resolve this correctly."
    }
  };

  const FAST_UR_TRANSLATE_LABELS = {
    en: "Translate",
    "pt-BR": "Traduzir",
    "pt-PT": "Traduzir",
    es: "Traducir",
    "es-419": "Traducir",
    fr: "Traduire",
    de: "Ubersetzen",
    it: "Tradurre",
    fi: "Kaantaa",
    hu: "Forditas",
    no: "Oversett",
    ro: "Traduce",
    ru: "Perevesti",
    bg: "Prevedi",
    id: "Terjemahkan",
    ja: "Translate",
    ko: "Translate",
    zh: "Translate",
    "zh-TW": "Translate",
    he: "Translate",
    nl: "Vertalen",
    pl: "Tlumacz",
    ar: "Translate",
    tr: "Cevir",
    th: "Translate",
    uk: "Pereklasty",
    cs: "Prelozit",
    el: "Translate",
    sv: "Oversatt",
    vi: "Dich",
    da: "Oversaet",
    hr: "Prevedi",
    sk: "Prelozit",
    sl: "Prevedi"
  };

  const SELECTORS = {
    activePanel: [
      ".overlay-container wz-card[class*='panel'].problem-edit",
      ".overlay-container .problem-edit",
      "wz-card[class*='panel'].problem-edit",
      ".problem-edit"
    ],
    issueTitle: [
      ".sub-title",
      '[data-testid="problem-title"]',
      "h1",
      "h2"
    ],
    description: [
      ".body .problem-data .description .content",
      '[data-testid="report-entry-description"]',
      ".description .content"
    ],
    comments: [
      ".body .conversation .comment .comment-text",
      ".body .conversation .comment p",
      "wz-list-item.comment .subtitle",
      ".comment-text",
      ".comment p"
    ],
    replyForm: [
      ".body .conversation .new-comment-form",
      ".new-comment-form"
    ],
    replyBox: [
      ".body .conversation .new-comment-text textarea",
      "textarea[id^='wz-textarea-']",
      "textarea"
    ],
    discussionHeader: [
      ".body .conversation .title",
      ".conversation.section .title",
      "[class*='conversation'] [class*='title']"
    ]
  };

  const STATE = {
    settings: null,
    observer: null,
    timer: null,
    root: null,
    detectedLanguage: "",
    translating: false
  };

  boot().catch((error) => {
    console.error("MapReply Copilot boot failed:", error);
  });

  async function boot() {
    STATE.settings = await loadSettings();
    injectStyles();
    ensureMounted();
    observeApp();
  }

  async function loadSettings() {
    const loaded = {};
    for (const key of Object.keys(DEFAULT_SETTINGS)) {
      loaded[key] = await GM_getValue(key, DEFAULT_SETTINGS[key]);
    }
    if (!loaded.editorLanguage) {
      loaded.editorLanguage = getUiLanguage();
    }
    return loaded;
  }

  async function saveSettings(patch) {
    STATE.settings = { ...STATE.settings, ...patch };
    for (const [key, value] of Object.entries(patch)) {
      await GM_setValue(key, value);
    }
  }

  function getUiLanguage() {
    const locale =
      window.I18n?.currentLocale?.() ||
      window.I18n?.locale ||
      navigator.language ||
      "en";
    return String(locale).split("-")[0].toLowerCase();
  }

  function getTranslateLabel() {
    const locale =
      window.I18n?.currentLocale?.() ||
      window.I18n?.locale ||
      navigator.language ||
      "en";
    return FAST_UR_TRANSLATE_LABELS[locale]
      || FAST_UR_TRANSLATE_LABELS[String(locale).split("-")[0]]
      || FAST_UR_TRANSLATE_LABELS.en;
  }

  function observeApp() {
    if (STATE.observer) {
      STATE.observer.disconnect();
    }

    STATE.observer = new MutationObserver((mutations) => {
      if (mutations.every((mutation) => isInsideMapReply(mutation.target))) {
        return;
      }
      scheduleEnsure();
    });

    STATE.observer.observe(document.body, {
      childList: true,
      subtree: true
    });
  }

  function scheduleEnsure() {
    window.clearTimeout(STATE.timer);
    STATE.timer = window.setTimeout(() => {
      ensureMounted();
    }, 250);
  }

  function isInsideMapReply(node) {
    return node instanceof HTMLElement && Boolean(node.closest(`#${ROOT_ID}`));
  }

  function getActivePanel() {
    for (const selector of SELECTORS.activePanel) {
      const node = document.querySelector(selector);
      if (node instanceof HTMLElement) {
        return node;
      }
    }
    return null;
  }

  function findReplyForm() {
    const panel = getActivePanel() || document;
    for (const selector of SELECTORS.replyForm) {
      const node = panel.querySelector(selector);
      if (node instanceof HTMLElement) {
        return node;
      }
    }
    return null;
  }

  function findReplyBox() {
    const panel = getActivePanel() || document;
    for (const selector of SELECTORS.replyBox) {
      const node = panel.querySelector(selector);
      if (node instanceof HTMLTextAreaElement) {
        return node;
      }
    }
    return null;
  }

  function findDiscussionHeader() {
    const panel = getActivePanel() || document;
    for (const selector of SELECTORS.discussionHeader) {
      const nodes = [...panel.querySelectorAll(selector)];
      const match = nodes.find((node) => {
        const text = (node.textContent || "").trim().toLowerCase();
        return text.includes("gesprek")
          || text.includes("conversation")
          || text.includes("comment")
          || text.includes("react");
      });
      if (match instanceof HTMLElement) {
        return match;
      }
    }
    return null;
  }

  function ensureMounted() {
    const replyForm = findReplyForm();
    if (!replyForm) {
      if (STATE.root?.isConnected) {
        STATE.root.remove();
      }
      STATE.root = null;
      return;
    }

    const existing = replyForm.querySelector(`#${ROOT_ID}`);
    if (existing instanceof HTMLElement) {
      STATE.root = existing;
      syncUi();
      ensureHeaderTranslateButton();
      return;
    }

    const root = document.createElement("div");
    root.id = ROOT_ID;
    root.className = "mrc-root";
    root.innerHTML = renderRootMarkup();

    const replyBox = findReplyBox();
    const boxWrap = replyBox?.closest(".new-comment-text, [class*='new-comment-text'], .wz-textarea");
    if (boxWrap instanceof HTMLElement && boxWrap.parentElement === replyForm) {
      boxWrap.insertAdjacentElement("beforebegin", root);
    } else {
      replyForm.insertAdjacentElement("beforeend", root);
    }

    root.addEventListener("click", handleRootClick);
    root.addEventListener("change", handleRootChange);

    STATE.root = root;
    syncUi();
    ensureHeaderTranslateButton();
  }

  function renderRootMarkup() {
    return `
      <div class="mrc-row mrc-row-top">
        <span class="mrc-kicker">MapReply</span>
        <span class="mrc-status" data-role="status">Klaar.</span>
      </div>
      <div class="mrc-row mrc-row-tools">
        <label class="mrc-template">
          <span>T</span>
          <select data-setting="selectedTemplate">
            ${Object.entries(TEMPLATE_LIBRARY)
              .map(([key, value]) => `<option value="${escapeHtml(key)}">${escapeHtml(value.label)}</option>`)
              .join("")}
          </select>
        </label>
        <button type="button" data-action="template">Invullen</button>
        <button type="button" data-action="translate-thread">${escapeHtml(getTranslateLabel())}</button>
        <button type="button" data-action="translate-draft">Vertaal reply</button>
      </div>
      <div class="mrc-row mrc-row-ai">
        <span class="mrc-ai-label">AI</span>
        <button type="button" data-action="ai-clarify">Vraag door</button>
        <button type="button" data-action="ai-answer">Antwoord</button>
        <button type="button" data-action="ai-close">Afsluiten</button>
      </div>
      <div class="mrc-results" data-role="results"></div>
    `;
  }

  function syncUi() {
    if (!STATE.root) {
      return;
    }
    const select = STATE.root.querySelector('[data-setting="selectedTemplate"]');
    if (select) {
      select.value = STATE.settings.selectedTemplate;
    }
  }

  function ensureHeaderTranslateButton() {
    const header = findDiscussionHeader();
    if (!header) {
      return;
    }

    const existing = header.querySelector(".mrc-header-translate");
    if (existing instanceof HTMLButtonElement) {
      existing.textContent = getTranslateLabel();
      return;
    }

    const button = document.createElement("button");
    button.type = "button";
    button.className = "mrc-header-translate";
    button.textContent = getTranslateLabel();
    button.addEventListener("click", async () => {
      try {
        await translateIncomingThread();
      } catch (error) {
        setStatus(error.message || String(error), true);
      }
    });
    header.appendChild(button);
  }

  async function handleRootClick(event) {
    const button = event.target.closest("[data-action]");
    if (!button) {
      return;
    }

    try {
      switch (button.getAttribute("data-action")) {
        case "template":
          applyTemplate();
          break;
        case "translate-thread":
          await translateIncomingThread();
          break;
        case "translate-draft":
          await translateDraftToReporter();
          break;
        case "ai-clarify":
          await generateAiReply("clarify");
          break;
        case "ai-answer":
          await generateAiReply("answer");
          break;
        case "ai-close":
          await generateAiReply("close");
          break;
        default:
          break;
      }
    } catch (error) {
      setStatus(error.message || String(error), true);
    }
  }

  async function handleRootChange(event) {
    const select = event.target.closest('[data-setting="selectedTemplate"]');
    if (!select) {
      return;
    }
    await saveSettings({ selectedTemplate: select.value });
    syncUi();
  }

  function collectContext() {
    const panel = getActivePanel() || document;
    const issueTitle = textFromSelectors(SELECTORS.issueTitle, panel);
    const descriptionNode = firstNodeFromSelectors(SELECTORS.description, panel);
    const description = normalizeText(descriptionNode?.textContent || "");
    const commentNodes = nodesFromSelectors(SELECTORS.comments, panel).slice(0, 12);
    const comments = commentNodes.map((node) => normalizeText(node.textContent || "")).filter(Boolean);
    const reporterMessage = [description, ...comments].filter(Boolean).join("\n\n").trim();

    return {
      panel,
      issueTitle,
      description,
      descriptionNode,
      commentNodes,
      comments,
      reporterMessage
    };
  }

  function textFromSelectors(selectors, scope) {
    for (const selector of selectors) {
      const node = scope.querySelector(selector);
      const text = normalizeText(node?.textContent || "");
      if (text) {
        return text;
      }
    }
    return "";
  }

  function firstNodeFromSelectors(selectors, scope) {
    for (const selector of selectors) {
      const node = scope.querySelector(selector);
      if (node instanceof HTMLElement) {
        return node;
      }
    }
    return null;
  }

  function nodesFromSelectors(selectors, scope) {
    for (const selector of selectors) {
      const nodes = [...scope.querySelectorAll(selector)].filter((node) => node instanceof HTMLElement);
      if (nodes.length) {
        return nodes;
      }
    }
    return [];
  }

  function normalizeText(value) {
    return String(value || "").replace(/\s+/g, " ").trim();
  }

  function applyTemplate() {
    const replyBox = findReplyBox();
    if (!replyBox) {
      setStatus("Geen replyveld gevonden.", true);
      return;
    }

    const template = TEMPLATE_LIBRARY[STATE.settings.selectedTemplate] || TEMPLATE_LIBRARY.clarify;
    setReplyBoxValue(replyBox, template.text);
    setStatus(`Template geladen: ${template.label}.`);
    renderResult(`<strong>Template</strong> ${escapeHtml(template.label)}`);
  }

  async function translateIncomingThread() {
    if (STATE.translating) {
      return;
    }

    STATE.translating = true;
    try {
      const context = collectContext();
      if (!context.reporterMessage) {
        throw new Error("Geen meldertekst gevonden in deze UR.");
      }

      clearInlineTranslations();
      const detection = await detectLanguage(context.reporterMessage);
      STATE.detectedLanguage = detection.language || "";
      if (!detection.language || detection.language === "unknown") {
        throw new Error("Taal van de melder kon niet worden herkend.");
      }

      if (normalizeLanguage(detection.language) === normalizeLanguage(STATE.settings.editorLanguage)) {
        setStatus(`Meldertaal is al ${detection.language}.`);
        renderResult(`<strong>Taal</strong> ${escapeHtml(detection.language)} | al gelijk aan editor`);
        return;
      }

      if (context.descriptionNode) {
        const translatedDescription = await translateText(context.description, STATE.settings.editorLanguage);
        insertInlineTranslation(context.descriptionNode, translatedDescription.translatedText, detection.language);
      }

      for (const node of context.commentNodes) {
        const original = normalizeText(node.textContent || "");
        if (!original) {
          continue;
        }
        const translated = await translateText(original, STATE.settings.editorLanguage);
        insertInlineTranslation(node, translated.translatedText, detection.language);
      }

      setStatus(`Gesprek vertaald uit ${detection.language}.`);
      renderResult(
        `<strong>Taal</strong> ${escapeHtml(detection.language)} | <strong>Editor</strong> ${escapeHtml(STATE.settings.editorLanguage)} | <strong>Zekerheid</strong> ${Math.round((detection.confidence || 0) * 100)}%`
      );
    } finally {
      STATE.translating = false;
    }
  }

  async function translateDraftToReporter() {
    const replyBox = findReplyBox();
    if (!replyBox) {
      throw new Error("Geen replyveld gevonden.");
    }

    const draft = replyBox.value.trim();
    if (!draft) {
      throw new Error("Schrijf eerst een reply.");
    }

    let targetLanguage = STATE.detectedLanguage;
    if (!targetLanguage) {
      const context = collectContext();
      if (!context.reporterMessage) {
        throw new Error("Geen meldertekst gevonden om de taal te bepalen.");
      }
      const detection = await detectLanguage(context.reporterMessage);
      targetLanguage = detection.language;
      STATE.detectedLanguage = targetLanguage;
    }

    if (!targetLanguage || targetLanguage === "unknown") {
      throw new Error("Meldertaal kon niet worden bepaald.");
    }

    if (normalizeLanguage(targetLanguage) === normalizeLanguage(STATE.settings.editorLanguage)) {
      setStatus("Reply hoeft niet vertaald te worden.");
      return;
    }

    const translated = await translateText(draft, targetLanguage);
    setReplyBoxValue(replyBox, translated.translatedText);
    setStatus(`Reply vertaald naar ${targetLanguage}.`);
    renderResult(`<strong>Reply vertaald</strong> ${escapeHtml(STATE.settings.editorLanguage)} -> ${escapeHtml(targetLanguage)}`);
  }

  async function generateAiReply(mode) {
    const apiKey = await ensureOpenAiKey();
    if (!apiKey) {
      throw new Error("OpenAI API key ontbreekt.");
    }

    const context = collectContext();
    if (!context.reporterMessage) {
      throw new Error("Geen genoeg UR-context gevonden voor AI.");
    }

    if (!STATE.detectedLanguage) {
      const detection = await detectLanguage(context.reporterMessage);
      STATE.detectedLanguage = detection.language || "";
    }

    const prompt = {
      mode,
      issueTitle: context.issueTitle,
      description: context.description,
      comments: context.comments,
      reporterLanguage: STATE.detectedLanguage || "unknown",
      editorLanguage: STATE.settings.editorLanguage
    };

    const response = await requestJson({
      method: "POST",
      url: "https://api.openai.com/v1/responses",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${apiKey}`
      },
      data: JSON.stringify({
        model: STATE.settings.aiModel,
        input: [
          {
            role: "system",
            content: [
              {
                type: "input_text",
                text: "You are a Waze Map Editor helper. Produce exactly one concise, practical reply draft for a user report. Be polite, concrete, and avoid overpromising. Return JSON only with keys reply and note."
              }
            ]
          },
          {
            role: "user",
            content: [
              {
                type: "input_text",
                text: JSON.stringify(prompt)
              }
            ]
          }
        ]
      })
    });

    const output = String(response.output_text || "").trim();
    if (!output) {
      throw new Error("AI gaf geen antwoord terug.");
    }

    const parsed = JSON.parse(output);
    if (!parsed.reply) {
      throw new Error("AI antwoord bevat geen reply.");
    }

    const replyBox = findReplyBox();
    if (!replyBox) {
      throw new Error("Geen replyveld gevonden.");
    }

    setReplyBoxValue(replyBox, parsed.reply);
    setStatus("AI reply ingevuld.");
    renderResult(`<strong>AI</strong> ${escapeHtml(parsed.note || mode)}`);
  }

  async function ensureOpenAiKey() {
    if (STATE.settings.openAiApiKey) {
      return STATE.settings.openAiApiKey;
    }

    const provided = window.prompt("Voer je OpenAI API key in voor MapReply AI:");
    if (!provided) {
      return "";
    }

    await saveSettings({ openAiApiKey: provided.trim() });
    return STATE.settings.openAiApiKey;
  }

  async function detectLanguage(text) {
    const data = await requestJson({
      method: "GET",
      url: buildGoogleTranslateUrl(text, "en", "auto")
    });

    return {
      language: typeof data?.[2] === "string" ? data[2] : "unknown",
      confidence: typeof data?.[6] === "number" ? data[6] : 0.75
    };
  }

  async function translateText(text, targetLanguage) {
    const data = await requestJson({
      method: "GET",
      url: buildGoogleTranslateUrl(text, targetLanguage, "auto")
    });

    const translatedText = Array.isArray(data?.[0])
      ? data[0]
          .filter((part) => Array.isArray(part) && typeof part[0] === "string")
          .map((part) => part[0])
          .join(" ")
          .trim()
      : "";

    return {
      translatedText,
      sourceLanguage: typeof data?.[2] === "string" ? data[2] : "unknown"
    };
  }

  function buildGoogleTranslateUrl(text, targetLanguage, sourceLanguage) {
    const url = new URL("https://translate.googleapis.com/translate_a/single");
    url.searchParams.set("client", "gtx");
    url.searchParams.set("sl", sourceLanguage);
    url.searchParams.set("tl", targetLanguage);
    url.searchParams.set("dt", "t");
    url.searchParams.set("q", text);
    return url.toString();
  }

  function requestJson({ method, url, headers = {}, data }) {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method,
        url,
        headers,
        data,
        onload: (response) => {
          if (response.status < 200 || response.status >= 300) {
            reject(new Error(`Request failed: ${response.status}`));
            return;
          }
          try {
            resolve(JSON.parse(response.responseText));
          } catch (error) {
            reject(new Error(`Invalid JSON from ${url}`));
          }
        },
        onerror: () => {
          reject(new Error(`Network request failed for ${url}`));
        }
      });
    });
  }

  function setReplyBoxValue(replyBox, value) {
    const descriptor = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value");
    if (descriptor?.set) {
      descriptor.set.call(replyBox, value);
    } else {
      replyBox.value = value;
    }
    replyBox.dispatchEvent(new Event("input", { bubbles: true }));
    replyBox.dispatchEvent(new Event("change", { bubbles: true }));
  }

  function insertInlineTranslation(anchorNode, translated, sourceLanguage) {
    if (!(anchorNode instanceof HTMLElement) || !anchorNode.parentElement) {
      return;
    }
    const helper = document.createElement("div");
    helper.className = "mrc-inline-translation";
    helper.innerHTML = `
      <div class="mrc-inline-translation-label">${escapeHtml(getTranslateLabel())} (${escapeHtml(sourceLanguage)} -> ${escapeHtml(STATE.settings.editorLanguage)})</div>
      <div class="mrc-inline-translation-text">${escapeHtml(translated)}</div>
    `;
    anchorNode.insertAdjacentElement("afterend", helper);
  }

  function clearInlineTranslations() {
    document.querySelectorAll(".mrc-inline-translation").forEach((node) => node.remove());
  }

  function renderResult(html) {
    const container = STATE.root?.querySelector('[data-role="results"]');
    if (!container) {
      return;
    }
    container.innerHTML = `<div class="mrc-result">${html}</div>`;
  }

  function setStatus(message, isError = false) {
    const node = STATE.root?.querySelector('[data-role="status"]');
    if (!node) {
      return;
    }
    node.textContent = message;
    node.dataset.error = isError ? "true" : "false";
  }

  function normalizeLanguage(value) {
    return String(value || "").trim().toLowerCase();
  }

  function escapeHtml(value) {
    return String(value || "")
      .replaceAll("&", "&amp;")
      .replaceAll("<", "&lt;")
      .replaceAll(">", "&gt;")
      .replaceAll('"', "&quot;")
      .replaceAll("'", "&#39;");
  }

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) {
      return;
    }

    const style = document.createElement("style");
    style.id = STYLE_ID;
    style.textContent = `
      .mrc-root {
        margin: 6px 0 8px;
        padding-top: 5px;
        border-top: 1px solid #e3e8ef;
        font: 12px/1.35 "Segoe UI", Tahoma, sans-serif;
        color: #4d5f73;
      }

      .mrc-row {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: 6px;
        margin-top: 4px;
      }

      .mrc-row-top {
        justify-content: space-between;
        gap: 10px;
      }

      .mrc-kicker {
        font-weight: 700;
        color: #5c6e81;
      }

      .mrc-status {
        margin-left: auto;
        font-size: 11px;
        color: #8b4b3f;
      }

      .mrc-status[data-error="true"] {
        color: #b94a48;
      }

      .mrc-template {
        display: inline-flex;
        align-items: center;
        gap: 5px;
      }

      .mrc-template span {
        width: 10px;
        font-weight: 700;
      }

      .mrc-template select,
      .mrc-root button,
      .mrc-header-translate {
        height: 28px;
        border: 1px solid #cfd5de;
        border-radius: 8px;
        background: linear-gradient(180deg, #ffffff 0%, #f5f7fa 100%);
        color: #566779;
        font: inherit;
        box-sizing: border-box;
      }

      .mrc-template select {
        min-width: 102px;
        max-width: 136px;
        padding: 3px 8px;
      }

      .mrc-root button,
      .mrc-header-translate {
        padding: 4px 9px;
        cursor: pointer;
      }

      .mrc-row-ai button {
        border-color: #bfd3ea;
        background: linear-gradient(180deg, #ffffff 0%, #eef4fb 100%);
      }

      .mrc-ai-label {
        font-weight: 700;
        color: #5c6e81;
        margin-right: 2px;
      }

      .mrc-results {
        margin-top: 6px;
      }

      .mrc-result {
        border-left: 2px solid #ccd6e2;
        background: #fafbfd;
        padding: 5px 7px;
        font-size: 11px;
      }

      .mrc-inline-translation {
        margin: 5px 0 8px;
        padding: 6px 8px;
        border-left: 3px solid #7fb1e6;
        background: #f7fbff;
        border-radius: 6px;
        color: #2a4a67;
        font: 12px/1.45 "Segoe UI", Tahoma, sans-serif;
      }

      .mrc-inline-translation-label {
        font-weight: 700;
        margin-bottom: 3px;
      }

      .mrc-header-translate {
        margin-left: 8px;
        vertical-align: middle;
      }
    `;

    document.head.appendChild(style);
  }
})();