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