T3 Chat Enhanced UI with Code Execution

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

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