T3 Chat Enhanced UI with Code Execution

Adds a zoomed-out preview scrollbar, code block list, download functionality, and code execution to T3 Chat

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         T3 Chat Enhanced UI with Code Execution
// @namespace    http://tampermonkey.net/
// @version      3.4
// @description  Adds a zoomed-out preview scrollbar, code block list, download functionality, and code execution to T3 Chat
// @author       T3 Chat
// @license      MIT
// @match        https://t3.chat/*
// @grant        none
// ==/UserScript==

(function () {
  "use strict";
  const C = {
    scale: 0.2,
    thumbHeightVh: 10,
    scrollbarOffset: 20,
    throttleDelay: 3000,
    codeListWidth: 300,
    codeListOffset: 20,
  };
  const EXT = {
    javascript: ".js", js: ".js", typescript: ".ts", ts: ".ts", python: ".py", py: ".py", java: ".java", csharp: ".cs", c: ".c", cpp: ".cpp", "c++": ".cpp", php: ".php", ruby: ".rb", rust: ".rs", go: ".go", html: ".html", css: ".css", scss: ".scss", sql: ".sql", json: ".json", xml: ".xml", yaml: ".yml", bash: ".sh", shell: ".sh", powershell: ".ps1", markdown: ".md", swift: ".swift", kotlin: ".kt", dart: ".dart", r: ".r", perl: ".pl", lua: ".lua", haskell: ".hs", scala: ".scala", elixir: ".ex", clojure: ".clj", dockerfile: "Dockerfile", makefile: "Makefile", plaintext: ".txt", text: ".txt"
  };
  const COMM = {
    javascript: "//", js: "//", typescript: "//", ts: "//", java: "//", csharp: "//", c: "//", cpp: "//", "c++": "//", go: "//", swift: "//", kotlin: "//", dart: "//", php: "//", python: "#", py: "#", ruby: "#", rust: "//", bash: "#", shell: "#", powershell: "#", r: "#", perl: "#", lua: "--", haskell: "--", sql: "--", elixir: "#", clojure: ";;", scala: "//", scss: "//", css: "/*", html: "<!--", xml: "<!--", yaml: "#", json: "", markdown: "", plaintext: "", text: ""
  };
  // Define runnable language types
  const RUNNABLE = {
    javascript: true,
    js: true,
    html: true
  };
  let state = { lastContentUpdate: 0, elements: {}, observers: {}, codeBlocks: [], codeBlockGroups: [] };
  if (window.t3ChatUICleanup) window.t3ChatUICleanup();

  function el(tag, css, html) {
    const e = document.createElement(tag);
    if (css) e.style.cssText = css;
    if (html) e.innerHTML = html;
    return e;
  }

  function createScrollbar() {
    const s = el("div", `position:fixed;top:0;right:${C.scrollbarOffset}px;width:150px;height:100vh;background:rgba(0,0,0,0.1);overflow:hidden;z-index:1000;`);
    s.id = "t3-chat-preview-scrollbar";
    const pc = el("div", `position:relative;transform:scale(${C.scale});transform-origin:top left;overflow:hidden;pointer-events:none;top:0;`);
    pc.id = "t3-chat-preview-content";
    s.appendChild(pc);
    const t = el("div", `position:absolute;top:0;left:0;width:100%;height:${C.thumbHeightVh}vh;background:rgba(66,135,245,0.5);cursor:grab;`);
    t.id = "t3-chat-preview-thumb";
    s.appendChild(t);
    document.body.appendChild(s);
    t.addEventListener("mousedown", handleThumbDrag);
    s.addEventListener("mousedown", handleScrollbarClick);
    return { scrollbar: s, previewContent: pc, thumb: t };
  }

  function createCodeList() {
    const main = document.querySelector("main");
    if (!main) return { codeList: null, listContainer: null };
    const cl = el("div", `position:relative;top:0;left:20px;width:fit-content;height:auto;background:rgba(0,0,0,0.2);overflow-y:auto;z-index:1000;font-family:system-ui,-apple-system,sans-serif;box-shadow:rgba(0,0,0,0.1) 2px 0px 5px;padding:10px;`);
    cl.id = "t3-chat-code-list";
    cl.appendChild(el("div", `font-size:16px;font-weight:bold;margin-bottom:15px;padding-bottom:8px;border-bottom:1px solid rgba(0,0,0,0.2);color:#fff;`, "Code Blocks"));
    const lc = el("div");
    lc.id = "t3-chat-code-list-container";
    cl.appendChild(lc);
    main.appendChild(cl);
    return { codeList: cl, listContainer: lc };
  }

  function detectLanguage(cb) {
    const p = cb.parentNode, l = p.querySelector(".font-mono");
    if (l) return l.textContent.trim().toLowerCase();
    const c = cb.getAttribute("data-language") || cb.className.match(/language-(\w+)/)?.[1];
    return c ? c.toLowerCase() : "";
  }

  function getFileExtension(l) { return EXT[l.toLowerCase()] || ".txt"; }
  function cleanCodeContent(c) { return c.replace(/^\s*\d+\s*\|/gm, "").trim(); }
  function downloadTextAsFile(f, t) {
    const a = el("a");
    a.href = "data:text/plain;charset=utf-8," + encodeURIComponent(t);
    a.download = f;
    a.style.display = "none";
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
  }

  // Create a container to display code execution results
  function createResultDisplay() {
    const existingDisplay = document.getElementById("t3-code-execution-result");
    if (existingDisplay) return existingDisplay;

    const display = el("div", `
      position: fixed;
      bottom: 20px;
      right: 20px;
      width: 400px;
      max-height: 300px;
      background: rgba(0, 0, 0, 0.8);
      color: #fff;
      border-radius: 8px;
      padding: 12px;
      font-family: monospace;
      z-index: 10000;
      overflow: auto;
      display: none;
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
    `);
    display.id = "t3-code-execution-result";

    const header = el("div", `
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 8px;
      padding-bottom: 8px;
      border-bottom: 1px solid rgba(255, 255, 255, 0.2);
    `, "<span>Code Execution Result</span>");

    const closeBtn = el("button", `
      background: transparent;
      border: none;
      color: #fff;
      cursor: pointer;
      font-size: 14px;
      padding: 2px 6px;
    `, "×");
    closeBtn.onclick = () => { display.style.display = "none"; };
    header.appendChild(closeBtn);

    const content = el("div", `white-space: pre-wrap;`);
    content.id = "t3-code-execution-content";

    display.appendChild(header);
    display.appendChild(content);
    document.body.appendChild(display);

    return display;
  }

  // Execute code safely
  function executeCode(code, language) {
    const resultDisplay = createResultDisplay();
    const resultContent = document.getElementById("t3-code-execution-content");
    resultDisplay.style.display = "block";

    // Capture console output
    const originalLog = console.log;
    const originalError = console.error;
    const originalWarn = console.warn;
    const originalInfo = console.info;

    let output = [];

    // Override console methods
    console.log = (...args) => {
      originalLog.apply(console, args);
      output.push(`<span style="color:#aaffaa;">LOG:</span> ${args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ')}`);
      updateOutput();
    };

    console.error = (...args) => {
      originalError.apply(console, args);
      output.push(`<span style="color:#ffaaaa;">ERROR:</span> ${args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ')}`);
      updateOutput();
    };

    console.warn = (...args) => {
      originalWarn.apply(console, args);
      output.push(`<span style="color:#ffdd99;">WARN:</span> ${args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ')}`);
      updateOutput();
    };

    console.info = (...args) => {
      originalInfo.apply(console, args);
      output.push(`<span style="color:#99ddff;">INFO:</span> ${args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ')}`);
      updateOutput();
    };

    function updateOutput() {
      resultContent.innerHTML = output.join('<br>');
      resultContent.scrollTop = resultContent.scrollHeight;
    }

    resultContent.innerHTML = '<span style="color:#aaaaff;">Executing code...</span>';

    // Special handling for HTML
    if (language === 'html') {
      try {
        // Create a sandbox iframe
        const sandbox = document.createElement('iframe');
        sandbox.style.cssText = 'width:100%;height:200px;border:none;';
        resultContent.innerHTML = '';
        resultContent.appendChild(sandbox);

        // Set the HTML content
        const iframeDocument = sandbox.contentDocument || sandbox.contentWindow.document;
        iframeDocument.open();
        iframeDocument.write(code);
        iframeDocument.close();

        output.push('<span style="color:#aaffaa;">HTML rendered in iframe</span>');
        updateOutput();
      } catch (error) {
        output.push(`<span style="color:#ffaaaa;">ERROR:</span> ${error.message}`);
        updateOutput();
      }
    } else {
      // For JavaScript
      try {
        // Execute the code
        const result = eval(code);

        // Show the return value if any
        if (result !== undefined) {
          let displayResult;
          try {
            displayResult = typeof result === 'object' ?
              JSON.stringify(result, null, 2) :
              String(result);
          } catch (e) {
            displayResult = '[Complex object]';
          }

          output.push(`<span style="color:#aaffff;">RESULT:</span> ${displayResult}`);
        }

        if (output.length === 0) {
          output.push('<span style="color:#aaffaa;">Code executed successfully with no output</span>');
        }
      } catch (error) {
        output.push(`<span style="color:#ffaaaa;">ERROR:</span> ${error.message}`);
      }

      updateOutput();
    }

    // Restore console methods
    setTimeout(() => {
      console.log = originalLog;
      console.error = originalError;
      console.warn = originalWarn;
      console.info = originalInfo;
    }, 0);
  }

  // Add run button to a code block element
  function addRunButtonToCodeBlock(pre, language, content) {
    // Check if we already added a run button
    if (pre.querySelector('.t3-run-code-btn')) return;

    // Only add run button for supported languages
    if (!RUNNABLE[language]) return;

    const runBtn = el("button", `
      position: absolute;
      top: 5px;
      right: 5px;
      background: rgba(66, 135, 245, 0.8);
      color: white;
      border: none;
      border-radius: 4px;
      padding: 3px 8px;
      font-size: 12px;
      cursor: pointer;
      opacity: 0.7;
      transition: opacity 0.2s;
      z-index: 10;
    `, "Run");

    runBtn.className = "t3-run-code-btn";
    runBtn.title = "Execute this code in the browser";

    runBtn.addEventListener("mouseover", () => {
      runBtn.style.opacity = "1";
    });

    runBtn.addEventListener("mouseout", () => {
      runBtn.style.opacity = "0.7";
    });

    runBtn.addEventListener("click", (e) => {
      e.stopPropagation();
      executeCode(content, language);
    });

    // Set the code block to relative positioning if not already set
    if (pre.style.position !== "relative") {
      pre.style.position = "relative";
    }

    pre.appendChild(runBtn);
  }

  function findCodeBlocks() {
    const groups = [];
    const seen = new Set();
    // Only search within the original document's log div, not in the preview
    const logDiv = document.querySelector('[role="log"]');
    if (!logDiv) return groups;

    [["Assistant message", "assistant"], ["Your message", "user"]].forEach(([aria, type]) => {
      logDiv.querySelectorAll(`div[aria-label="${aria}"]`).forEach((msg, mi) => {
        const pres = msg.querySelectorAll(".shiki.not-prose");
        if (!pres.length) return;
        const blocks = [];
        pres.forEach((pre, bi) => {
          if (seen.has(pre)) return;
          seen.add(pre);
          const code = pre.querySelector("code");
          if (!code) return;
          const content = cleanCodeContent(code.textContent || "");
          const lines = content.split("\n");
          const lang = detectLanguage(pre), comm = COMM[lang] || "";
          let name = lines[0]?.trim() || `Code Block ${bi + 1}`;
          if (comm && name.startsWith(comm)) name = name.slice(comm.length).trim();
          if (name.length > 20) name = name.slice(0, 17) + "...";
          const ext = getFileExtension(lang);
          if (ext && !name.endsWith(ext)) name += ext;

          // Add run button to the code block in the chat
          addRunButtonToCodeBlock(pre, lang, content);

          blocks.push({ id: `${type}-msg-${mi}-block-${bi}`, name, language: lang, element: pre, content, messageType: type });
        });
        if (blocks.length) groups.push({ messageType: type, messageIndex: mi, blocks });
      });
    });
    return groups;
  }

  function updateCodeList() {
    const { listContainer } = state.elements;
    if (!listContainer) return;
    const groups = findCodeBlocks();
    state.codeBlockGroups = groups;
    state.codeBlocks = groups.flatMap(g => g.blocks);
    listContainer.innerHTML = "";
    if (!groups.length) {
      listContainer.appendChild(el("div", `color:#666;font-style:italic;padding:10px 0;text-align:center;`, "No code blocks found"));
      return;
    }
    groups.forEach(g => {
      listContainer.appendChild(el("div", `font-size:14px;font-weight:bold;margin-top:15px;margin-bottom:8px;padding:5px;border-radius:4px;color:white;background:${g.messageType === "assistant" ? "rgba(66,135,245,0.6)" : "rgba(120,120,120,0.6)"};`, `${g.messageType === "assistant" ? "Assistant" : "User"} Message #${g.messageIndex + 1}`));
      g.blocks.forEach(b => {
        const item = el("div", `display:flex;justify-content:space-between;align-items:center;padding:8px;margin-bottom:8px;background:${b.messageType === "assistant" ? "rgba(0,0,0,0.4)" : "rgba(60,60,60,0.4)"};border-radius:4px;cursor:pointer;transition:background-color 0.2s;border-left:3px solid ${b.messageType === "assistant" ? "rgba(66,135,245,0.8)" : "rgba(180,180,180,0.8)"};`);
        item.addEventListener("mouseover", () => {
          item.style.backgroundColor = b.messageType === "assistant"
            ? "rgba(66,135,245,0.2)"
            : "rgba(120,120,120,0.2)";
        });
        item.addEventListener("mouseout", () => {
          item.style.backgroundColor = b.messageType === "assistant"
            ? "rgba(0,0,0,0.4)"
            : "rgba(60,60,60,0.4)";
        });

        const nameSpan = el("div", `flex-grow:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:14px;`);
        if (b.language) nameSpan.appendChild(el("span", `background:rgba(66,135,245,0.3);color:white;font-size:10px;padding:1px 4px;border-radius:3px;margin-right:5px;`, b.language));
        nameSpan.appendChild(document.createTextNode(b.name));
        const btns = el("div", `display:flex;gap:5px;`);

        // Copy
        const copyBtn = el("button", `background:transparent;border:none;color:rgba(66,135,245,0.8);cursor:pointer;font-size:14px;padding:2px 5px;border-radius:3px;`, `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"></path></svg>`);
        copyBtn.title = "Copy code";
        copyBtn.onclick = e => {
          e.stopPropagation();
          navigator.clipboard.writeText(b.content).then(() => {
            const o = copyBtn.innerHTML;
            copyBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="green" stroke-width="2"><path d="M20 6L9 17l-5-5"></path></svg>`;
            setTimeout(() => { copyBtn.innerHTML = o; }, 1500);
          });
        };

        // Download
        const dlBtn = el("button", `background:transparent;border:none;color:rgba(66,135,245,0.8);cursor:pointer;font-size:14px;padding:2px 5px;border-radius:3px;`, `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>`);
        dlBtn.title = "Download code";
        dlBtn.onclick = e => {
          e.stopPropagation();
          downloadTextAsFile(b.name, b.content);
          const o = dlBtn.innerHTML;
          dlBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="green" stroke-width="2"><path d="M20 6L9 17l-5-5"></path></svg>`;
          setTimeout(() => (dlBtn.innerHTML = o), 1500);
        };

        // Run (only for supported languages)
        if (RUNNABLE[b.language]) {
          const runBtn = el("button", `background:transparent;border:none;color:rgba(66,135,245,0.8);cursor:pointer;font-size:14px;padding:2px 5px;border-radius:3px;`, `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>`);
          runBtn.title = "Run code";
          runBtn.onclick = e => {
            e.stopPropagation();
            executeCode(b.content, b.language);
            const o = runBtn.innerHTML;
            runBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="green" stroke-width="2"><path d="M20 6L9 17l-5-5"></path></svg>`;
            setTimeout(() => (runBtn.innerHTML = o), 1500);
          };
          btns.appendChild(runBtn);
        }

        btns.appendChild(copyBtn);
        btns.appendChild(dlBtn);
        item.appendChild(nameSpan);
        item.appendChild(btns);
        item.onclick = () => {
          b.element.scrollIntoView({ behavior: "smooth", block: "center" });
          const o = b.element.style.outline, ob = b.element.style.backgroundColor;
          b.element.style.outline = "3px solid rgba(66,135,245,0.8)";
          b.element.style.backgroundColor = "rgba(66,135,245,0.1)";
          setTimeout(() => {
            b.element.style.outline = o;
            b.element.style.backgroundColor = ob;
          }, 2000);
        };
        listContainer.appendChild(item);
      });
    });
  }

  function updatePreviewContent() {
    const { logDiv, previewContent } = state.elements;
    if (!logDiv || !previewContent) return;
    const now = Date.now();
    if (now - state.lastContentUpdate < C.throttleDelay) return;
    const clone = logDiv.cloneNode(true);
    clone.querySelectorAll("*").forEach(e => { e.style.pointerEvents = "none"; e.style.userSelect = "none"; });
    clone.querySelectorAll(".shiki.not-prose").forEach(e => { e.style.outline = "2px solid #ff9800"; e.style.background = "rgba(255,152,0,0.2)"; e.style.borderRadius = "4px"; });
    previewContent.innerHTML = "";
    previewContent.appendChild(clone);
    previewContent.style.width = logDiv.getBoundingClientRect().width + "px";
    state.lastContentUpdate = now;
  }

  function updateThumbPosition() {
    const { scrollParent, thumb, scrollbar, previewContent } = state.elements;
    if (!scrollParent || !thumb || !scrollbar || !previewContent) return;
    const sh = scrollParent.scrollHeight, ch = scrollParent.clientHeight, st = scrollParent.scrollTop, sbh = scrollbar.offsetHeight, th = (C.thumbHeightVh / 100) * window.innerHeight;
    let tp = (sbh - th) * (st / (sh - ch));
    tp = Math.min(Math.max(0, tp), sbh - th);
    thumb.style.height = th + "px";
    thumb.style.top = tp + "px";
    const tc = tp + th / 2;
    previewContent.style.top = `-${st * C.scale - tc}px`;
  }

  function handleThumbDrag(e) {
    const { scrollParent, thumb, scrollbar, previewContent } = state.elements;
    if (!scrollParent || !thumb || !scrollbar || !previewContent) return;
    const sh = scrollParent.scrollHeight, ch = scrollParent.clientHeight, sbh = scrollbar.offsetHeight, th = (C.thumbHeightVh / 100) * window.innerHeight, sy = e.clientY, st = thumb.offsetTop;
    function drag(ev) {
      const dy = ev.clientY - sy, nt = Math.max(0, Math.min(st + dy, sbh - th));
      thumb.style.top = nt + "px";
      const sp = (nt / (sbh - th)) * (sh - ch);
      scrollParent.scrollTop = sp;
      const tc = nt + th / 2;
      previewContent.style.top = `-${sp * C.scale - tc}px`;
    }
    function stop() {
      document.removeEventListener("mousemove", drag);
      document.removeEventListener("mouseup", stop);
    }
    document.addEventListener("mousemove", drag);
    document.addEventListener("mouseup", stop);
    e.preventDefault();
  }

  function handleScrollbarClick(e) {
    if (e.target === state.elements.thumb) return;
    const { scrollParent, thumb, scrollbar } = state.elements;
    if (!scrollParent || !thumb || !scrollbar) return;
    const r = scrollbar.getBoundingClientRect(), y = e.clientY - r.top, th = (C.thumbHeightVh / 100) * window.innerHeight, tc = thumb.offsetTop + th / 2, dy = y - tc;
    scrollParent.scrollTop = Math.max(0, Math.min(scrollParent.scrollTop + dy / C.scale, scrollParent.scrollHeight - scrollParent.clientHeight));
    updateThumbPosition();
  }

  function updateDimensions() {
    const { logDiv, scrollbar, previewContent } = state.elements;
    if (!logDiv || !scrollbar || !previewContent) return;
    const w = logDiv.getBoundingClientRect().width;
    scrollbar.style.width = w * C.scale + "px";
    previewContent.style.width = w + "px";
    updatePreviewContent();
    updateThumbPosition();
    state.observers.height = requestAnimationFrame(updateDimensions);
  }

  function initialize() {
    const s = createScrollbar(), c = createCodeList(), logDiv = document.querySelector('[role="log"]'), scrollParent = logDiv?.parentNode;
    state.elements = { ...s, ...c, logDiv, scrollParent };
    if (!logDiv || !scrollParent) return;

    // Initial update of code list
    updateCodeList();

    // Set up observers
    state.observers.content = new MutationObserver(() => {
      updatePreviewContent();
      updateCodeList();
    });
    state.observers.content.observe(logDiv, { childList: true, subtree: true, characterData: true });

    window.addEventListener("resize", () => {
      updatePreviewContent();
      updateCodeList();
    });

    scrollParent.addEventListener("scroll", updateThumbPosition);
    updateDimensions();
  }

  function cleanup() {
    const { scrollbar, scrollParent, codeList } = state.elements;
    if (scrollbar) scrollbar.remove();
    if (codeList) codeList.remove();
    const main = document.querySelector("main");
    if (main) main.style.paddingLeft = "";
    window.removeEventListener("resize", updatePreviewContent);
    if (scrollParent) scrollParent.removeEventListener("scroll", updateThumbPosition);
    if (state.observers.content) state.observers.content.disconnect();
    if (state.observers.height) cancelAnimationFrame(state.observers.height);

    // Clean up the result display
    const resultDisplay = document.getElementById("t3-code-execution-result");
    if (resultDisplay) resultDisplay.remove();

    state = { lastContentUpdate: 0, elements: {}, observers: {}, codeBlocks: [], codeBlockGroups: [] };
  }

  function waitForLogDiv() {
    const logDiv = document.querySelector('[role="log"]');
    if (logDiv) initialize();
    else setTimeout(waitForLogDiv, 200);
  }

  window.t3ChatUICleanup = () => { cleanup(); delete window.t3ChatUICleanup; };
  waitForLogDiv();
})();