DeepZer Flows Compact Sender TEMP

Versión temporal exclusiva para DeepZer Flows: conserva el mensaje completo y muestra la burbuja enviada como Fix error.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         DeepZer Flows Compact Sender TEMP
// @namespace    https://deepzerr.com/lovable-compact-sender/deepzer-flows
// @version      1.0.0
// @license      MIT
// @description  Versión temporal exclusiva para DeepZer Flows: conserva el mensaje completo y muestra la burbuja enviada como Fix error.
// @author       DeepZer
// @match        https://lovable.dev/projects/cec453fc-d962-4e70-817b-a68f4bcfe8bb*
// @run-at       document-start
// @noframes
// @sandbox      raw
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  const VERSION = '1.0.0';
  const PROJECT_ID = 'cec453fc-d962-4e70-817b-a68f4bcfe8bb';
  const STATUS_ID = 'dzf-compact-status';
  const STYLE_ID = 'dzf-compact-styles';
  const FORM_SELECTOR = 'form#chat-input';
  const FETCH_MARKER = '__dzfCompactFetchVersion';
  const BOOT_MARKER = '__dzfCompactBootVersion';
  const VISUAL_TIMEOUT_MS = 12000;
  const CHIP_TEXT = 'Fix error';

  if (!location.pathname.includes(`/projects/${PROJECT_ID}`)) return;
  if (window[BOOT_MARKER] === VERSION) return;

  window[BOOT_MARKER] = VERSION;

  function log(...args) {
    console.log(`[DeepZer Flows Compact v${VERSION}]`, ...args);
  }

  function warn(...args) {
    console.warn(`[DeepZer Flows Compact v${VERSION}]`, ...args);
  }

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

  function isChatRequest(input, init) {
    const method = String(
      init?.method ||
      (input instanceof Request ? input.method : 'GET')
    ).toUpperCase();

    if (method !== 'POST') return false;

    const rawUrl =
      typeof input === 'string'
        ? input
        : input instanceof URL
          ? input.href
          : input?.url || '';

    try {
      const url = new URL(rawUrl, location.href);

      const lovableHost =
        url.hostname === 'lovable.dev' ||
        url.hostname.endsWith('.lovable.dev');

      return lovableHost && url.pathname.includes('/chat');
    } catch (_) {
      const value = String(rawUrl);

      return (
        value.includes('lovable.dev') &&
        value.includes('/chat')
      );
    }
  }

  function decoratePayload(payload) {
    if (
      !payload ||
      typeof payload !== 'object' ||
      typeof payload.message !== 'string' ||
      !payload.message.trim()
    ) {
      return null;
    }

    const originalMessage = payload.message;
    const buildState = window.__lcsBuildState || {};
    const fallbackId = String(payload.id || '');
    const eventId = String(buildState.eventId || fallbackId);

    /*
     * El texto original no se modifica.
     * Lovable recibe la instrucción completa.
     */
    payload.intent = 'fix_error';

    payload.message_intent_metadata = {
      fix_error_metadata: {
        errors: [
          {
            error_type: 'build',
            error_message: String(
              buildState.errorMessage || originalMessage
            ),
            build_event_id: eventId
          }
        ]
      }
    };

    payload.contains_error = true;
    payload.error_source = 'build_errors';
    payload.error_ids = eventId ? [eventId] : [];

    return {
      payload,
      originalMessage
    };
  }

  function modifyStringBody(body) {
    if (typeof body !== 'string') return null;

    try {
      const payload = JSON.parse(body);
      const result = decoratePayload(payload);

      if (!result) return null;

      return {
        body: JSON.stringify(result.payload),
        originalMessage: result.originalMessage
      };
    } catch (_) {
      return null;
    }
  }

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

    const style = document.createElement('style');

    style.id = STYLE_ID;

    style.textContent = `
      #${STATUS_ID} {
        display: flex;
        align-items: center;
        gap: 7px;
        width: max-content;
        margin: 6px 0 0 8px;
        padding: 0;
        border: 0 !important;
        background: transparent !important;
        box-shadow: none !important;
        color: #8a8a93;
        font: 500 11px/1 Inter, system-ui, -apple-system,
              BlinkMacSystemFont, "Segoe UI", sans-serif;
        pointer-events: none;
        user-select: none;
      }

      #${STATUS_ID} .dzf-status-dot {
        width: 7px;
        height: 7px;
        flex: 0 0 7px;
        border-radius: 999px;
        background: #22c55e;
        animation: dzf-status-pulse 1.45s ease-in-out infinite;
      }

      .dzf-fix-chip {
        display: inline-flex !important;
        align-items: center !important;
        justify-content: center !important;
        width: max-content !important;
        min-width: 0 !important;
        max-width: 180px !important;
        min-height: 32px !important;
        padding: 7px 13px !important;
        border: 1px solid rgba(148, 163, 184, 0.22) !important;
        border-radius: 999px !important;
        background: rgba(44, 44, 46, 0.96) !important;
        color: #d7d7dc !important;
        font-size: 13px !important;
        font-style: italic !important;
        font-weight: 500 !important;
        line-height: 1 !important;
        white-space: nowrap !important;
        overflow: hidden !important;
        text-overflow: ellipsis !important;
      }

      @keyframes dzf-status-pulse {
        0%, 100% {
          opacity: 1;
          box-shadow:
            0 0 0 0 rgba(34, 197, 94, 0.50);
        }

        50% {
          opacity: 0.55;
          box-shadow:
            0 0 0 5px rgba(34, 197, 94, 0);
        }
      }
    `;

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

  function mountStatus() {
    const form = document.querySelector(FORM_SELECTOR);

    if (!form?.isConnected) return false;

    injectStyles();

    let status = document.getElementById(STATUS_ID);

    if (!status) {
      status = document.createElement('div');
      status.id = STATUS_ID;
    }

    status.title =
      `DeepZer Flows Compact Sender v${VERSION} activo`;

    status.innerHTML = `
      <span
        class="dzf-status-dot"
        aria-hidden="true"
      ></span>
      <span>En uso · Flows TEMP v${VERSION}</span>
    `;

    if (status.previousElementSibling !== form) {
      form.insertAdjacentElement('afterend', status);
    }

    return true;
  }

  function mountStatusWithRetries(attempt = 0) {
    if (mountStatus() || attempt >= 20) return;

    window.setTimeout(
      () => mountStatusWithRetries(attempt + 1),
      400
    );
  }

  function isVisible(element) {
    if (!(element instanceof Element)) return false;

    const rect = element.getBoundingClientRect();
    const style = getComputedStyle(element);

    return (
      rect.width > 0 &&
      rect.height > 0 &&
      style.display !== 'none' &&
      style.visibility !== 'hidden'
    );
  }

  function messageMatches(text, originalMessage) {
    const candidate = normalizeText(text);
    const original = normalizeText(originalMessage);

    if (!candidate || !original) return false;

    if (candidate === original) return true;

    const prefix = original.slice(
      0,
      Math.min(100, original.length)
    );

    const suffix = original.slice(
      Math.max(0, original.length - 60)
    );

    return (
      candidate.includes(prefix) &&
      (
        original.length < 120 ||
        !suffix ||
        candidate.includes(suffix)
      )
    );
  }

  function findNewestUserMessage(originalMessage) {
    const containers = Array.from(
      document.querySelectorAll('div[data-message-id]')
    ).filter((container) => {
      if (!isVisible(container)) return false;

      const userGroup =
        container.querySelector('.group\\/user-message');

      if (!userGroup) return false;

      return messageMatches(
        container.textContent,
        originalMessage
      );
    });

    if (!containers.length) return null;

    return containers.reduce((latest, current) => {
      if (!latest) return current;

      const latestRect =
        latest.getBoundingClientRect();

      const currentRect =
        current.getBoundingClientRect();

      return currentRect.bottom >= latestRect.bottom
        ? current
        : latest;
    }, null);
  }

  function findBubbleInside(container, originalMessage) {
    if (!(container instanceof Element)) return null;

    const nativeChip =
      container.querySelector('div.special-message');

    if (nativeChip) return nativeChip;

    const preferred = Array.from(
      container.querySelectorAll(
        'div[class*="bg-secondary-pulse"], ' +
        'div[class*="whitespace-pre-wrap"], ' +
        'div[class*="rounded-br-1"], ' +
        'div[class*="max-w-[300px]"]'
      )
    ).filter((element) => {
      if (!isVisible(element)) return false;
      if (element.closest(FORM_SELECTOR)) return false;

      return messageMatches(
        element.textContent,
        originalMessage
      );
    });

    if (preferred.length) {
      return preferred.reduce((best, current) => {
        if (!best) return current;

        return current.contains(best)
          ? current
          : best;
      }, null);
    }

    const all = Array.from(
      container.querySelectorAll('div, p, span')
    )
      .filter((element) => {
        if (!isVisible(element)) return false;
        if (element.closest(FORM_SELECTOR)) return false;

        return messageMatches(
          element.textContent,
          originalMessage
        );
      })
      .sort(
        (a, b) =>
          b.querySelectorAll('*').length -
          a.querySelectorAll('*').length
      );

    return all[0] || null;
  }

  function convertToFixChip(target) {
    if (!(target instanceof Element)) return false;

    const chip =
      target.matches('div.special-message')
        ? target
        : (() => {
            target.replaceChildren();

            const element =
              document.createElement('div');

            element.className =
              'special-message dzf-fix-chip';

            target.appendChild(element);

            return element;
          })();

    chip.textContent = CHIP_TEXT;
    chip.classList.add('dzf-fix-chip');
    chip.dataset.dzfCompact = VERSION;
    chip.setAttribute('aria-label', CHIP_TEXT);
    chip.title = CHIP_TEXT;

    return true;
  }

  function compactOutgoingMessage(originalMessage) {
    injectStyles();

    const startedAt = Date.now();

    let observer = null;
    let stopped = false;

    const stop = () => {
      if (stopped) return;

      stopped = true;
      observer?.disconnect();
    };

    const inspect = () => {
      if (stopped) return true;

      const container =
        findNewestUserMessage(originalMessage);

      if (container) {
        const bubble =
          findBubbleInside(
            container,
            originalMessage
          );

        if (
          bubble &&
          convertToFixChip(bubble)
        ) {
          log(
            'Burbuja de DeepZer Flows convertida a Fix error.'
          );

          stop();
          return true;
        }
      }

      if (
        Date.now() - startedAt >=
        VISUAL_TIMEOUT_MS
      ) {
        warn(
          'No se encontró la nueva burbuja dentro del tiempo de búsqueda.'
        );

        stop();
      }

      return false;
    };

    const begin = () => {
      if (
        stopped ||
        !document.body
      ) {
        return;
      }

      observer = new MutationObserver(() => {
        inspect();
      });

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

      [
        0,
        60,
        120,
        240,
        450,
        800,
        1300,
        2100,
        3500,
        5500,
        8000,
        11000
      ].forEach((delay) => {
        window.setTimeout(inspect, delay);
      });

      window.setTimeout(
        stop,
        VISUAL_TIMEOUT_MS + 300
      );
    };

    if (document.body) {
      begin();
    } else {
      document.addEventListener(
        'DOMContentLoaded',
        begin,
        {
          once: true
        }
      );
    }
  }

  function installFetchInterceptor() {
    const currentFetch = window.fetch;

    if (typeof currentFetch !== 'function') {
      return false;
    }

    if (
      currentFetch[FETCH_MARKER] === VERSION
    ) {
      return true;
    }

    async function dzfFetch(input, init) {
      if (!isChatRequest(input, init)) {
        return currentFetch.call(
          window,
          input,
          init
        );
      }

      try {
        const modified =
          modifyStringBody(init?.body);

        if (modified) {
          /*
           * La búsqueda visual empieza antes de
           * entregar la solicitud a Lovable.
           */
          compactOutgoingMessage(
            modified.originalMessage
          );

          return currentFetch.call(
            window,
            input,
            {
              ...init,
              body: modified.body
            }
          );
        }

        if (input instanceof Request) {
          const rawBody =
            await input.clone().text();

          const requestModified =
            modifyStringBody(rawBody);

          if (requestModified) {
            compactOutgoingMessage(
              requestModified.originalMessage
            );

            const nextRequest =
              new Request(input, {
                body: requestModified.body
              });

            return currentFetch.call(
              window,
              nextRequest,
              init
            );
          }
        }
      } catch (error) {
        warn(
          'Se mantuvo el envío original por seguridad.',
          error
        );
      }

      return currentFetch.call(
        window,
        input,
        init
      );
    }

    Object.defineProperty(
      dzfFetch,
      FETCH_MARKER,
      {
        value: VERSION,
        configurable: false,
        enumerable: false,
        writable: false
      }
    );

    window.fetch = dzfFetch;

    log(
      'Interceptor temporal instalado para DeepZer Flows.'
    );

    return (
      window.fetch[FETCH_MARKER] === VERSION
    );
  }

  installFetchInterceptor();

  window.setTimeout(
    installFetchInterceptor,
    1200
  );

  window.setTimeout(
    installFetchInterceptor,
    3500
  );

  if (
    document.readyState === 'loading'
  ) {
    document.addEventListener(
      'DOMContentLoaded',
      mountStatusWithRetries,
      {
        once: true
      }
    );
  } else {
    mountStatusWithRetries();
  }

  document.addEventListener(
    'focusin',
    (event) => {
      if (
        event.target?.closest?.(
          FORM_SELECTOR
        )
      ) {
        mountStatus();
      }
    },
    true
  );
})();