您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
adds buttons for recently used models to the sidebar
// ==UserScript== // @name t3chat recent models // @namespace https://t3.chat/ // @version 2025-07-28 // @description adds buttons for recently used models to the sidebar // @author arturmarc // @match https://t3.chat/* // @icon https://t3.chat/favicon.ico // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; const NUM_RECENT_MODELS = 4; const $$ = document.querySelectorAll.bind(document); const $ = (selector) => { const el = document.querySelector(selector); if (!(el instanceof HTMLElement || el instanceof SVGElement)) { throw new Error( `[t3chatCustom] Element not found for selector: ${selector}` ); } return el; }; const asElementOrSvgItems = (nodeList) => [...nodeList].filter( (el) => el instanceof HTMLElement || el instanceof SVGElement ); const clickElement = (element) => { if (!element) { console.warn("[t3chatCustom] Cannot click null/undefined element"); return false; } try { element.focus(); const keyEvent = new KeyboardEvent("keydown", { key: "Enter", code: "Enter", bubbles: true, cancelable: true }); element.dispatchEvent(keyEvent); return true; } catch (error) { console.warn("[t3chatCustom] Click failed:", error); return false; } }; function assertExists(value, message) { if (value === null || value === void 0) { console.error(message); throw new Error(message); } } async function waitForElements(selector) { const els = await waitForSelectorAll(selector); const res = [...els].filter( (el) => el instanceof HTMLElement ); if (res.length === 0) { throw new Error( `[t3chatCustom] No elements found for selector: ${selector}` ); } return res; } function waitForSelectorAll(selector) { return new Promise((resolve, reject) => { const immediateResult = asElementOrSvgItems($$(selector)); if (immediateResult.length > 0) { return resolve(immediateResult); } const observer = new MutationObserver(() => { const res = asElementOrSvgItems($$(selector)); if (res.length > 0) { observer.disconnect(); clearTimeout(timeout); resolve(res); } }); const timeout = setTimeout(() => { observer.disconnect(); reject( new Error( `[t3chatCustom] Selector "${selector}" not found within 500ms` ) ); }, 500); observer.observe(document.body, { childList: true, subtree: true }); }); } const waitForSelector = async (selector) => { const elements = await waitForSelectorAll(selector); return elements[0]; }; const createModelButton = ({ modelName, modelMenu, newChatButton }) => { const svg = modelSVGSs.get(modelName); if (!svg) { console.log(`[t3chatCustom] skipping model: ${modelName} - svg not found`); return null; } const svgElement = svg.cloneNode(true); const button = document.createElement("button"); button.className = newChatButton.className; newChatButton.parentElement?.classList.add("flex", "flex-col", "gap-2"); button.appendChild(svgElement); button.appendChild(document.createTextNode(modelName)); button.addEventListener("click", () => { clickElement(newChatButton); clickElement(modelMenu); waitForElements( "div[data-radix-menu-content] div[data-radix-collection-item]" ).then((items) => { const foundItem = items.find( (item) => item.querySelector("span")?.textContent === modelName ); if (foundItem) { clickElement(foundItem); } }); }); return button; }; const STORAGE_KEY = "t3chatCustom:recentModels"; const modelSVGSs = /* @__PURE__ */ new Map(); const getAllModelSVGSs = (modelMenuItems) => { modelMenuItems.forEach((item) => { const svg = item.querySelector("svg"); if (svg) { modelSVGSs.set( (item.querySelector("span")?.textContent || "").trim(), svg ); } }); }; const renderRecentModels = async () => { let extraButtonsContainer = $$("[data-t3chat-custom-extra-buttons]")[0]; const startingHtml = ` <div data-sidebar="group-label" class="flex h-8 shrink-0 select-none items-center rounded-md text-xs font-medium outline-none ring-sidebar-ring transition-[margin,opa] duration-200 ease-snappy focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0 group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 px-3.5 text-color-heading pt-4"> <span>Recent models</span> </div> `; if (extraButtonsContainer) { extraButtonsContainer.innerHTML = startingHtml; } const links = await waitForElements("a[href='/']"); const newChatButton = links.find( (link) => (link.textContent ?? "").trim().toLocaleLowerCase().includes("new chat") ); if (!newChatButton) { console.log("[t3chatCustom] new chat button not found"); return; } const modelMenu = (await waitForSelector("button[aria-haspopup] > svg.lucide-chevron-down")).parentElement; assertExists(modelMenu, "modelMenu not found"); let currentModelName = modelMenu.textContent; const storageItem = localStorage.getItem(STORAGE_KEY); let recentModels = []; if (!storageItem) { clickElement(modelMenu); const modelMenuItems = await waitForSelectorAll( "div[data-radix-menu-content] div[data-radix-collection-item]" ); getAllModelSVGSs(modelMenuItems); const defaultRecentModels = [ currentModelName || "", // take 5 items from the model menu items as default recent models ...[...modelMenuItems].map((item) => item.querySelector("span")?.textContent || "").filter((item) => item !== currentModelName).slice(0, 3) ]; clickElement(modelMenu); localStorage.setItem(STORAGE_KEY, JSON.stringify(defaultRecentModels)); recentModels = defaultRecentModels; } else { recentModels = JSON.parse(storageItem); } if (!extraButtonsContainer) { const newExtraButtonsContainer = document.createElement("div"); newExtraButtonsContainer.className = "flex flex-col gap-2"; newExtraButtonsContainer.setAttribute( "data-t3chat-custom-extra-buttons", "" ); newChatButton.parentElement?.insertAdjacentElement( "afterend", newExtraButtonsContainer ); extraButtonsContainer = newExtraButtonsContainer; extraButtonsContainer.innerHTML = startingHtml; } const recentModelsToRender = recentModels.slice(0, NUM_RECENT_MODELS); if (!recentModelsToRender.every((model) => modelSVGSs.has(model))) { clickElement(modelMenu); try { const modelMenuItems = await waitForSelectorAll( "div[data-radix-menu-content] div[data-radix-collection-item]" ); getAllModelSVGSs(modelMenuItems); clickElement(modelMenu); } catch (error) { console.log("[t3chatCustom] error getting model menu items", error); } } recentModelsToRender.forEach((model) => { if (!modelSVGSs.has(model)) { console.log( "[t3chatCustom] skipping - model svg not found for model", model ); } }); recentModels.filter((model) => modelSVGSs.has(model)).slice(0, NUM_RECENT_MODELS).forEach((model) => { const button = createModelButton({ modelName: model, modelMenu, newChatButton }); if (button) { extraButtonsContainer.appendChild(button); } }); }; waitForSelector("button[aria-haspopup] > svg.lucide-chevron-down").then( async (dropdownSvg) => { const modelMenu = dropdownSvg.parentElement; assertExists(modelMenu, "modelMenu not found"); renderRecentModels(); let sidebarRendered = $$("div[data-sidebar]").length > 0; let extraButtonsContainerRendered = $$("[data-t3chat-custom-extra-buttons]").length > 0; let modelMenuRendered = true; const mutationObserver = new MutationObserver( (mutations) => { const sidebarRenderedAfterMutation = $$("div[data-sidebar]").length > 0; if (sidebarRenderedAfterMutation && !sidebarRendered) { renderRecentModels(); } sidebarRendered = sidebarRenderedAfterMutation; const extraButtonsContainerRenderedAfterMutation = $$("[data-t3chat-custom-extra-buttons]").length > 0; if (!extraButtonsContainerRenderedAfterMutation && extraButtonsContainerRendered) { renderRecentModels(); } extraButtonsContainerRendered = extraButtonsContainerRenderedAfterMutation; const allRemovedNodes = mutations.flatMap((mutation) => [ ...mutation.removedNodes ]); const modelMenuRemoved = allRemovedNodes.some( (node) => node instanceof HTMLElement && node.querySelector( "button[aria-haspopup] > svg.lucide-chevron-down" ) ); if (modelMenuRemoved) { modelMenuObserver?.disconnect(); } const modelMenuRenderedAfterMutation = $$("button[aria-haspopup] > svg.lucide-chevron-down").length > 0; if (!modelMenuRenderedAfterMutation && modelMenuRendered) { modelMenuObserver?.disconnect(); } if (modelMenuRenderedAfterMutation && (!modelMenuRendered || modelMenuRemoved)) { observeModelMenu( $$("button[aria-haspopup] > svg.lucide-chevron-down")[0].parentElement ); } modelMenuRendered = modelMenuRenderedAfterMutation; } ); mutationObserver.observe(document.body, { childList: true, subtree: true }); observeModelMenu(modelMenu); } ); let modelMenuObserver = null; function observeModelMenu(modelMenu) { let currentModelName = (modelMenu.textContent ?? "").replace( /(\w)\(/g, "$1 (" // text before the bracket is missing a space ); console.log("observing model menu again, currentModelName", currentModelName); const setRecentModels = () => { const recentModels = JSON.parse( localStorage.getItem(STORAGE_KEY) ?? "[]" ); const newRecentModels = [ currentModelName, ...recentModels.filter((model) => model !== currentModelName).slice(0, 8) ]; localStorage.setItem(STORAGE_KEY, JSON.stringify(newRecentModels)); }; setRecentModels(); modelMenuObserver = new MutationObserver(() => { if (modelMenu.textContent !== currentModelName) { currentModelName = (modelMenu.textContent ?? "").replace( /(\w)\(/g, "$1 (" // text before the bracket is missing a space ); setRecentModels(); renderRecentModels(); } }); modelMenuObserver.observe(modelMenu, { childList: true, subtree: true, characterData: true }); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址