您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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(); })();