MapReply Copilot

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

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==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);
  }
})();