有没有能监控模型用量的脚本呢?

比如Grok或者ChatGPT、Gemini这些,有没有能监控还能对话几次,什么时候恢复次数的油猴脚本或者插件呢?

6 Likes

有的,我找一下

2 Likes

grok

Gemini好像用不太完吧;gpt?gpt降智,不用,如果要用的话,我记得有个检测pow值的插件,会显示剩余次数,不过它还有广告,就不推荐了

1 Like

谢谢佬,GPT确实用着不爽

1 Like

gpt
闭源脚本

2 Likes

找到一个开源脚本,但是需要改一下,统计算法有问题,而且只有按天计算的模型

// ==UserScript==
// @name         ChatGPT Model Usage Monitor
// @namespace    https://github.com/tizee/tampermonkey-chatgpt-model-usage-monitor
// @downloadURL  https://raw.githubusercontent.com/tizee/tampermonkey-chatgpt-model-usage-monitor/main/monitor.js
// @updateURL    https://raw.githubusercontent.com/tizee/tampermonkey-chatgpt-model-usage-monitor/main/monitor.js
// @icon         https://www.google.com/s2/favicons?sz=64&domain=chatgpt.com
// @author       tizee
// @version      1.8.2
// @description  Elegant usage monitor for ChatGPT models with daily quota tracking
// @match        https://chatgpt.com/
// @match        https://chatgpt.com/c/*
// @match        https://chatgpt.com/g/*/c/*
// @match        https://chatgpt.com/g/*/project?=*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function () {
    "use strict";
    // text-scramble animation
    (()=>{var TextScrambler=(()=>{var l=Object.defineProperty;var c=Object.getOwnPropertyDescriptor;var u=Object.getOwnPropertyNames;var m=Object.prototype.hasOwnProperty;var d=(n,t)=>{for(var e in t)l(n,e,{get:t[e],enumerable:!0})},f=(n,t,e,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of u(t))!m.call(n,i)&&i!==e&&l(n,i,{get:()=>t[i],enumerable:!(s=c(t,i))||s.enumerable});return n};var g=n=>f(l({},"__esModule",{value:!0}),n);var T={};d(T,{default:()=>r});function _(n){let t=document.createTreeWalker(n,NodeFilter.SHOW_TEXT,{acceptNode:s=>s.nodeValue.trim()?NodeFilter.FILTER_ACCEPT:NodeFilter.FILTER_SKIP}),e=[];for(;t.nextNode();)t.currentNode.nodeValue=t.currentNode.nodeValue.replace(/(\n|\r|\t)/gm,""),e.push(t.currentNode);return e}function p(n,t,e){return t<0||t>=n.length?n:n.substring(0,t)+e+n.substring(t+1)}function M(n,t){return n?"x":t[Math.floor(Math.random()*t.length)]}var r=class{constructor(t,e={}){this.el=t;let s={duration:1e3,delay:0,reverse:!1,absolute:!1,pointerEvents:!0,scrambleSymbols:"\u2014~\xB1\xA7|[].+$^@*()\u2022x%!?#",randomThreshold:null};this.config=Object.assign({},s,e),this.config.randomThreshold===null&&(this.config.randomThreshold=this.config.reverse?.1:.8),this.textNodes=_(this.el),this.nodeLengths=this.textNodes.map(i=>i.nodeValue.length),this.originalText=this.textNodes.map(i=>i.nodeValue).join(""),this.mask=this.originalText.split(" ").map(i=>"\xA0".repeat(i.length)).join(" "),this.currentMask=this.mask,this.totalChars=this.originalText.length,this.scrambleRange=Math.floor(this.totalChars*(this.config.reverse?.25:1.5)),this.direction=this.config.reverse?-1:1,this.config.absolute&&(this.el.style.position="absolute",this.el.style.top="0"),this.config.pointerEvents||(this.el.style.pointerEvents="none"),this._animationFrame=null,this._startTime=null,this._running=!1}initialize(){return this.currentMask=this.mask,this}_getEased(t){let e=-(Math.cos(Math.PI*t)-1)/2;return e=Math.pow(e,2),this.config.reverse?1-e:e}_updateScramble(t,e,s){if(Math.random()<.5&&t>0&&t<1)for(let i=0;i<20;i++){let o=i/20,a;if(this.config.reverse?a=e-Math.floor((1-Math.random())*this.scrambleRange*o):a=e+Math.floor((1-Math.random())*this.scrambleRange*o),!(a<0||a>=this.totalChars)&&this.currentMask[a]!==" "){let h=Math.random()>this.config.randomThreshold?this.originalText[a]:M(this.config.reverse,this.config.scrambleSymbols);this.currentMask=p(this.currentMask,a,h)}}}_composeOutput(t,e,s){let i="";if(this.config.reverse){let o=Math.max(e-s,0);i=this.mask.slice(0,o)+this.currentMask.slice(o,e)+this.originalText.slice(e)}else i=this.originalText.slice(0,e)+this.currentMask.slice(e,e+s)+this.mask.slice(e+s);return i}_updateTextNodes(t){let e=0;for(let s=0;s<this.textNodes.length;s++){let i=this.nodeLengths[s];this.textNodes[s].nodeValue=t.slice(e,e+i),e+=i}}_tick=t=>{this._startTime||(this._startTime=t);let e=t-this._startTime,s=Math.min(e/this.config.duration,1),i=this._getEased(s),o=Math.floor(this.totalChars*s),a=Math.floor(2*(.5-Math.abs(s-.5))*this.scrambleRange);this._updateScramble(s,o,a);let h=this._composeOutput(s,o,a);this._updateTextNodes(h),s<1?this._animationFrame=requestAnimationFrame(this._tick):this._running=!1};start(){this._running=!0,this._startTime=null,this.config.delay?setTimeout(()=>{this._animationFrame=requestAnimationFrame(this._tick)},this.config.delay):this._animationFrame=requestAnimationFrame(this._tick)}stop(){this._animationFrame&&(cancelAnimationFrame(this._animationFrame),this._animationFrame=null),this._running=!1}};return g(T);})();
          window.TextScrambler = TextScrambler.default || TextScrambler;
         })();


    // Constants and Configuration
    const COLORS = {
        primary: "#5E9EFF",
        background: "#1A1B1E",
        surface: "#2A2B2E",
        border: "#363636",
        text: "#E5E7EB",
        secondaryText: "#9CA3AF",
        success: "#10B981",
        warning: "#F59E0B",
        danger: "#EF4444",
        disabled: "#4B5563",
        white: "oklch(.928 .006 264.531)",
        gray: "oklch(.92 .004 286.32)",
        yellow: "oklch(.905 .182 98.111)",
        green: "oklch(.845 .143 164.978)",
        // Red for low usage
        progressLow: "#EF4444",
        // Orange for medium usage
        progressMed: "#F59E0B",
        // Green for high usage
        progressHigh: "#10B981",
        // Gray for exceeded
        progressExceed: "#4B5563",
    };

    const STYLE = {
        borderRadius: "12px",
        boxShadow:
        "0 4px 6px -1px rgba(0, 0, 0, 0.2), 0 2px 4px -1px rgba(0, 0, 0, 0.1)",
        spacing: {
            xs: "4px",
            sm: "8px",
            md: "16px",
            lg: "24px",
        },
        textSize: {
            xs: "0.75rem",
            sm: "0.875rem",
            md: "1rem",
        },
        lineHeight: {
            xs: "calc(1/.75)",
            sm: "calc(1.25/.875)",
            md: "1.5",
        },
    };

    // Helper Functions
    const getToday = () => new Date().toISOString().split("T")[0];

    // Default Configuration
    const defaultUsageData = {
        position: { x: null, y: null },
        lastReset: getToday(),
        progressType: "dots", // dots or bar
        models: {
            "o3-mini": {
                displayName: "o3-mini",
                count: 0,
                dailyLimit: 150,
                lastUpdate: "",
            },
            "o3-mini-high": {
                displayName: "o3-mini-high",
                count: 0,
                dailyLimit: 50,
                lastUpdate: "",
            },
        },
    };
    // Updated Styles
    GM_addStyle(`
  #chatUsageMonitor {
    position: fixed;
    bottom: ${STYLE.spacing.lg};
    right: ${STYLE.spacing.lg};
    width: 360px;
    max-height: 500px;
    overflow-y: scroll;
    background: ${COLORS.background};
    color: ${COLORS.text};
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    border-radius: ${STYLE.borderRadius};
    box-shadow: ${STYLE.boxShadow};
    z-index: 9999;
    border: 1px solid ${COLORS.border};
    user-select: none;
  }

  #chatUsageMonitor header {
    padding: 0 ${STYLE.spacing.md};
    display: flex;
    border-radius: ${STYLE.borderRadius} ${STYLE.borderRadius} 0 0;
    background: ${COLORS.background};
    flex-direction: row;
    position: relative;
  }

  #chatUsageMonitor .drag-handle {
    width: 12px;
    height: 12px;
    position: absolute;
    right: 8px;
    top: 50%;
    transform: translateY(-50%);
    border-radius: 50%;
    background: ${COLORS.secondaryText};
    cursor: move;
    transition: background-color 0.2s ease;
  }

  #chatUsageMonitor .drag-handle:hover {
    background: ${COLORS.yellow};
  }

  #chatUsageMonitor header button {
    border: none;
    background: none;
    color: ${COLORS.secondaryText};
    cursor: pointer;
    font-weight: 500;
    transition: color 0.2s ease;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-right: ${STYLE.spacing.sm};
    padding-top:  ${STYLE.spacing.sm};
  }

  #chatUsageMonitor header button.active {
    color: ${COLORS.yellow};
  }

  #chatUsageMonitor .content {
    padding: ${STYLE.spacing.xs} ${STYLE.spacing.md};
    overflow-y: auto;
  }

   #chatUsageMonitor input {
    width: 80px;
    padding: ${STYLE.spacing.xs} ${STYLE.spacing.sm};
    margin: 0;
    border: none;
    border-radius: 0;
    background: transparent;
    color: ${COLORS.secondaryText};
    font-family: monospace;
    font-size: ${STYLE.textSize.xs};
    line-height: ${STYLE.lineHeight.xs};
    transition: color 0.2s ease;
  }

  #chatUsageMonitor input:focus {
    outline: none;
    color: ${COLORS.yellow};
    background: transparent;
  }

  #chatUsageMonitor input:hover {
    color: ${COLORS.yellow};
  }



  #chatUsageMonitor .btn {
    padding: ${STYLE.spacing.sm} ${STYLE.spacing.md};
    border: none;
    cursor: pointer;
    color: ${COLORS.white};
    font-weight: 500;
    font-size: ${STYLE.textSize.sm};
    transition: all 0.2s ease;
    text-decoration: underline;
  }

  #chatUsageMonitor .btn:hover {
    color: ${COLORS.yellow};
  }

  #chatUsageMonitor .delete-btn {
    padding: ${STYLE.spacing.xs} ${STYLE.spacing.sm};
    margin-left: ${STYLE.spacing.sm};
  }

  #chatUsageMonitor .delete-btn.btn:hover {
     color: ${COLORS.danger};
  }

  #chatUsageMonitor::-webkit-scrollbar {
    width: 8px;
  }

  #chatUsageMonitor::-webkit-scrollbar-track {
    background: ${COLORS.surface};
    border-radius: 4px;
  }

  #chatUsageMonitor::-webkit-scrollbar-thumb {
    background: ${COLORS.border};
    border-radius: 4px;
  }

  #chatUsageMonitor::-webkit-scrollbar-thumb:hover {
    background: ${COLORS.secondaryText};
  }

    #chatUsageMonitor .progress-container {
        width: 100%;
        background: ${COLORS.surface};
        margin-top: ${STYLE.spacing.xs};
        border-radius: 6px;
        overflow: hidden;
        height: 8px; /* Slightly taller for better visibility */
        position: relative;
    }

  #chatUsageMonitor .progress-bar {
        height: 100%;
        transition: width 0.3s ease;
        border-radius: 6px;
        background: linear-gradient(
            90deg,
            ${COLORS.progressLow} 0%,
            ${COLORS.progressMed} 50%,
            ${COLORS.progressHigh} 100%
        );
        background-size: 200% 100%;
        animation: gradientShift 2s linear infinite;
  }

 #chatUsageMonitor .progress-bar.low-usage {
        animation: pulse 1.5s ease-in-out infinite;
    }

    #chatUsageMonitor .progress-bar.exceeded {
        background: ${COLORS.progressExceed};
        animation: none;
    }

    @keyframes gradientShift {
        0% { background-position: 100% 0; }
        100% { background-position: -100% 0; }
    }

    @keyframes pulse {
        0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); }
        70% { box-shadow: 0 0 0 6px rgba(239, 68, 68, 0); }
        100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
    }

 /* Dot-based progression system */
    #chatUsageMonitor .dot-progress {
        display: flex;
        gap: 4px;
        align-items: center;
        height: 8px;
    }

    #chatUsageMonitor .dot {
        width: 8px;
        height: 8px;
        border-radius: 50%;
        transition: all 0.3s ease;
    }

    #chatUsageMonitor .dot-empty {
        background: rgba(239, 68, 68, 0.3);
        border: 1px solid ${COLORS.progressLow};
    }

    #chatUsageMonitor .dot-partial {
        background: ${COLORS.progressMed};
    }

    #chatUsageMonitor .dot-full {
        background: ${COLORS.progressHigh};
    }

    #chatUsageMonitor .dot-exceeded {
        background: ${COLORS.progressExceed};
        position: relative;
    }

    #chatUsageMonitor .dot-exceeded::before {
        content: '';
        position: absolute;
        top: 50%;
        left: -2px;
        right: -2px;
        height: 2px;
        background: ${COLORS.surface};
        transform: rotate(45deg);
    }

  #chatUsageMonitor .table-header {
    font-family: monospace;
    color: ${COLORS.white};
    font-size:  ${STYLE.textSize.xs};
    line-height: ${STYLE.lineHeight.xs};
    display : grid;
    align-items: center;
    grid-template-columns: 2fr 1.5fr 1.5fr 2fr;
  }
 #chatUsageMonitor .model-row {
    font-family: monospace;
    color: ${COLORS.secondaryText};
    transition: color 0.2s ease;
    font-size:  ${STYLE.textSize.xs};
    line-height: ${STYLE.lineHeight.xs};
    display : grid;
    grid-template-columns: 2fr 1.5fr 1.5fr 2fr;
    align-items: center;
  }
  #chatUsageMonitor .model-row:hover {
    color: ${COLORS.yellow};
    text-decoration-line: underline;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  }
  /* Container to help position the arrow (pseudo-element) */
   #chatUsageMonitor .custom-select {
      position: relative;
      display: inline-block;
    }

  /* Hide the native select arrow and style the dropdown */
   #chatUsageMonitor .custom-select select {
      -webkit-appearance: none; /* Safari and Chrome */
      -moz-appearance: none;    /* Firefox */
      appearance: none;         /* Standard modern browsers */

      background-color: transparent;
      color: #ffffff;
      border: none;
      cursor: pointer;
      color: ${COLORS.white};
      font-size: ${STYLE.textSize.md};
      line-height:  ${STYLE.lineHeight.md};
    }
    /* Style the list of options (when the dropdown is open) */
    .custom-select select option {
       background: ${COLORS.background};
      color: ${COLORS.white};
    }

    /* Optional: highlight the hovered option in some browsers */
    .custom-select select option:hover {
      background: ${COLORS.background};
     color: ${COLORS.yellow};
    text-decoration-line: underline;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
    }

    #chatUsageMonitor input {
    width: 90%;
    padding: ${STYLE.spacing.xs} ${STYLE.spacing.sm};
    margin: 0;
    border: 1px solid ${COLORS.border};
    border-radius: 4px;
    background: ${COLORS.surface};
    color: ${COLORS.secondaryText};
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    font-size: ${STYLE.textSize.xs};
    line-height: ${STYLE.lineHeight.xs};
    transition: all 0.2s ease;
  }

  #chatUsageMonitor input:focus {
    outline: none;
    border-color: ${COLORS.yellow};
    color: ${COLORS.yellow};
    background: rgba(245, 158, 11, 0.1);
  }

  #chatUsageMonitor input:hover {
    border-color: ${COLORS.yellow};
    color: ${COLORS.yellow};
  }

  /* Toast notification for feedback */
  #chatUsageMonitor .toast {
    position: absolute;
    bottom: 20px;
    left: 50%;
    transform: translateX(-50%);
    background: ${COLORS.background};
    color: ${COLORS.success};
    padding: ${STYLE.spacing.sm} ${STYLE.spacing.md};
    border-radius: ${STYLE.borderRadius};
    border: 1px solid ${COLORS.success};
    opacity: 0;
    transition: opacity 0.3s ease;
    z-index: 10000;
  }

  #chatUsageMonitor .toast.show {
    opacity: 1;
  }
`);

    // State Management
    const Storage = {
        key: "usageData",

        get() {
            let usageData = GM_getValue(this.key, defaultUsageData);
            if (!usageData){
                usageData = defaultUsageData;
            };
            if (!usageData.position) {
                usageData.position = { x: null, y: null };
                this.set(usageData);
            }
            if (!usageData.progressType) {
                usageData.progressType = "dots";
                this.set(usageData);
            }
            console.debug("[monitor] get usageData:", usageData);
            return usageData;
        },

        set(newData) {
            GM_setValue(this.key, newData);
        },

        update(callback) {
            const data = this.get();
            callback(data);
            this.set(data);
        }
    };

    let usageData = Storage.get();
    // Component Functions
    function createModelRow(model, modelKey, isSettings = false) {
        const row = document.createElement("div");
        row.className = "model-row";

        if (isSettings) {
            return createSettingsModelRow(model, modelKey, row);
        }
        return createUsageModelRow(model, modelKey, row);
    }

    function createSettingsModelRow(model, modelKey, row) {
        // Model ID cell
        const keyLabel = document.createElement("div");
        keyLabel.textContent = modelKey;
        row.appendChild(keyLabel);

        // Display Name input cell (new field)
        const nameInput = document.createElement("input");
        nameInput.type = "text";
        nameInput.value = model.displayName || modelKey;
        nameInput.placeholder = "Display Name";
        nameInput.dataset.modelKey = modelKey;
        nameInput.dataset.field = "displayName";
        row.appendChild(nameInput);

        // Daily Limit cell
        const limitInput = document.createElement("input");
        limitInput.type = "number";
        limitInput.value = model.dailyLimit;
        limitInput.placeholder = "limit";
        limitInput.dataset.modelKey = modelKey;
        limitInput.dataset.field = "dailyLimit";
        row.appendChild(limitInput);

        // Delete button cell
        const delBtn = document.createElement("button");
        delBtn.className = "btn delete-btn";
        delBtn.textContent = "Delete";
        delBtn.dataset.modelKey = modelKey;
        delBtn.addEventListener("click", () => handleDeleteModel(modelKey));
        row.appendChild(delBtn);

        return row;
    }

    function createUsageModelRow(model, modelKey) {
        const row = document.createElement("div");
        row.className = "model-row";

        // Model Name cell
        const modelName = document.createElement("div");
        modelName.textContent = model.displayName;
        row.appendChild(modelName);

        // Last Update cell
        const lastUpdateValue = document.createElement("div");
        lastUpdateValue.textContent = model.lastUpdate || "??";
        row.appendChild(lastUpdateValue);

        // Usage cell
        const usageValue = document.createElement("div");
        const dailyLimitDisplay = model.dailyLimit > 0 ? model.dailyLimit : "∞";
        usageValue.textContent = `${model.count} / ${dailyLimitDisplay}`;
        row.appendChild(usageValue);

        // Progress Bar cell (retain default font)
        const progressCell = document.createElement("div");
        if (model.dailyLimit > 0) {
            const usagePercent = model.count / model.dailyLimit;
            console.debug("[monitor] progress type", usageData.progressType);
            if (usageData.progressType == "dots") {
                // Dot-based progress implementation
                const dotContainer = document.createElement("div");
                dotContainer.className = "dot-progress";
                const totalDots = 8;

                for (let i = 0; i < totalDots; i++) {
                    const dot = document.createElement("div");
                    dot.className = "dot";

                    const dotThreshold = (i + 1) / totalDots;
                    if (usagePercent >= 1) {
                        dot.classList.add("dot-exceeded");
                    } else if (usagePercent >= dotThreshold) {
                        dot.classList.add("dot-full");
                    } else if (usagePercent >= dotThreshold - 0.1) {
                        dot.classList.add("dot-partial");
                    } else {
                        dot.classList.add("dot-empty");
                    }

                    dotContainer.appendChild(dot);
                }
                progressCell.appendChild(dotContainer);
            } else {
                // Enhanced progress bar implementation
                const progressContainer = document.createElement("div");
                progressContainer.className = "progress-container";

                const progressBar = document.createElement("div");
                progressBar.className = "progress-bar";

                if (usagePercent > 1) {
                    progressBar.classList.add("exceeded");
                } else if (usagePercent < 0.3) {
                    progressBar.classList.add("low-usage");
                }

                progressBar.style.width = `${Math.min(usagePercent * 100, 100)}%`;

                progressContainer.appendChild(progressBar);
                progressCell.appendChild(progressContainer);
            }
        } else {
            progressCell.style.width = `100%`;
        }
        row.appendChild(progressCell);

        return row;
    }
    // Update the getStatusColor function to match new color logic
    function getStatusColor(remainingPercent, hasLimit) {
        if (!hasLimit) return COLORS.progressHigh;
        const usagePercent = 1 - remainingPercent;

        if (remainingPercent < 0) return COLORS.progressExceed;
        if (usagePercent <= 0.3) return COLORS.progressLow;
        if (usagePercent <= 0.7) return COLORS.progressMed;
        return COLORS.progressHigh;
    }


    // Event Handlers
    function handleDeleteModel(modelKey) {
        if (confirm(`Delete mapping for model "${modelKey}"?`)) {
            delete usageData.models[modelKey];
            Storage.set(usageData);
            updateUI();
            showToast(`Model "${modelKey}" deleted.`);
        }
    }

    function animateText(el, config)
    {
        const animator = new TextScrambler(el, {...config});
        animator.initialize();
        animator.start();
    }

    // UI Updates
    function updateUI() {
        const usageContent = document.getElementById("usageContent");
        const settingsContent = document.getElementById("settingsContent");

        if (usageContent) {
            console.debug("[monitor] update usage");
            updateUsageContent(usageContent);
            animateText(usageContent, { duration: 500, delay: 0, reverse: false, absolute: false, pointerEvents: true });
        }

        if (settingsContent) {
            console.debug("[monitor] update setting");
            updateSettingsContent(settingsContent);
            animateText(settingsContent, { duration: 500, delay: 0, reverse: false, absolute: false, pointerEvents: true });
        }

    }

    let sortDescending = true;

    function updateUsageContent(container) {
        container.innerHTML = "";

        // Title Section
        const subtitle = document.createElement("div");
        subtitle.textContent = `${getToday()}`;
        subtitle.style.fontSize = `${STYLE.textSize.xs}`;
        subtitle.style.lineHeight = `${STYLE.lineHeight.xs}`;
        subtitle.style.color = `${COLORS.secondaryText}`;
        container.appendChild(subtitle);

        // Table Header Row (logging window header)
        const tableHeader = document.createElement("div");
        tableHeader.className = "table-header";

        // Header cells:
        const modelNameHeader = document.createElement("div");
        modelNameHeader.textContent = "Model Name";
        tableHeader.appendChild(modelNameHeader);
        const lastUpdateHeader = document.createElement("div");
        lastUpdateHeader.textContent = "Update";
        tableHeader.appendChild(lastUpdateHeader);
        const usageHeader = document.createElement("div");
        usageHeader.textContent = sortDescending ? "Usage ↓" : "Usage ↑";
        usageHeader.style.cursor = "pointer";
        usageHeader.addEventListener("click", () => {
            sortDescending = !sortDescending;
            updateUsageContent(container);
        });
        tableHeader.appendChild(usageHeader);
        const progressHeader = document.createElement("div");
        progressHeader.textContent = "Progress";
        tableHeader.appendChild(progressHeader);
        container.appendChild(tableHeader);

        // Sort models by usage count (descending by default)
        const sortedModels = Object.entries(usageData.models).sort(
            ([, a], [, b]) => {
                return sortDescending ? b.count - a.count : a.count - b.count;
            }
        );

        // Create a row for each model
        sortedModels.forEach(([modelKey, model]) => {
            const row = createUsageModelRow(model, modelKey);
            container.appendChild(row);
        });

        if (sortedModels.length === 0) {
            const emptyState = document.createElement("div");
            emptyState.style.textAlign = "center";
            emptyState.style.color = COLORS.secondaryText;
            emptyState.style.padding = STYLE.spacing.lg;
            emptyState.textContent = "No models configured. Add some in Settings.";
            container.appendChild(emptyState);
        }
    }

    function updateSettingsContent(container) {
        container.innerHTML = "";

        const info = document.createElement("p");
        info.textContent = "Configure model mappings and daily limits:";
        info.style.fontSize = STYLE.textSize.md;
        info.style.fontSize = STYLE.lineHeight.md;
        info.style.color = COLORS.secondaryText;
        container.appendChild(info);

        // Add table header for settings
        const tableHeader = document.createElement("div");
        tableHeader.className = "table-header";

        const idHeader = document.createElement("div");
        idHeader.textContent = "Model ID";
        tableHeader.appendChild(idHeader);

        const nameHeader = document.createElement("div");
        nameHeader.textContent = "Display Name";
        tableHeader.appendChild(nameHeader);

        const limitHeader = document.createElement("div");
        limitHeader.textContent = "Daily Limit";
        tableHeader.appendChild(limitHeader);

        const actionHeader = document.createElement("div");
        actionHeader.textContent = "Action";
        tableHeader.appendChild(actionHeader);

        container.appendChild(tableHeader);

        // Update CSS to accommodate the new column layout
        GM_addStyle(`
      #chatUsageMonitor .table-header,
      #chatUsageMonitor .model-row {
        grid-template-columns: 1.5fr 1.5fr 1fr 1fr;
      }
    `);

        Object.entries(usageData.models).forEach(([modelKey, model]) => {
            const row = createModelRow(model, modelKey, true);
            container.appendChild(row);
        });

        // add new model
        const addBtn = document.createElement("button");
        addBtn.className = "btn";
        addBtn.textContent = "Add Model Mapping";
        addBtn.addEventListener("click", () => {
            const newModelID = prompt(
                'Enter new model internal ID (e.g., "o3-mini")'
            );
            if (!newModelID) return;

            if (usageData.models[newModelID]) {
                alert("Model mapping already exists.");
                return;
            }

            usageData.models[newModelID] = {
                displayName: newModelID,
                count: 0,
                dailyLimit: undefined,
                lastUpdate: "",
            };

            Storage.set(usageData);
            updateUI();
        });
        container.appendChild(addBtn);

        // save model limits
        const saveBtn = document.createElement("button");
        saveBtn.className = "btn";
        saveBtn.textContent = "Save Settings";
        saveBtn.style.marginLeft = STYLE.spacing.sm;
        saveBtn.addEventListener("click", () => {
            const inputs = container.querySelectorAll("input");
            let hasChanges = false;

            inputs.forEach((input) => {
                const modelKey = input.dataset.modelKey;
                const field = input.dataset.field;

                if (!modelKey || !usageData.models[modelKey]) return;

                if (field === "displayName") {
                    const newDisplayName = input.value.trim();
                    if (
                        newDisplayName &&
                        newDisplayName !== usageData.models[modelKey].displayName
                    ) {
                        usageData.models[modelKey].displayName = newDisplayName;
                        hasChanges = true;
                    }
                } else if (field === "dailyLimit") {
                    const newLimit = parseInt(input.value, 10);
                    if (
                        !isNaN(newLimit) &&
                        newLimit !== usageData.models[modelKey].dailyLimit
                    ) {
                        usageData.models[modelKey].dailyLimit = newLimit;
                        hasChanges = true;
                    }
                }
            });

            if (hasChanges) {
                Storage.set(usageData);
                updateUI();
                showToast("Settings saved successfully.");
            } else {
                showToast("No changes detected.", "warning");
            }
        });
        container.appendChild(saveBtn);

        const selectContainer = document.createElement("div");
        const progressTypeSelect = document.createElement("select");
        selectContainer.className = "custom-select";
        progressTypeSelect.innerHTML = `
        <option value="dots" selected>dots</option>
        <option value="bar">bar</option>
        `;
        progressTypeSelect.value = usageData.progressType || "dots";
        progressTypeSelect.addEventListener('change', () => {
            usageData.progressType = progressTypeSelect.value;
            Storage.set(usageData);
            updateUI();
            console.debug('[monitor] progress type:', progressTypeSelect.value);
        });
        progressTypeSelect.style.marginLeft = STYLE.spacing.sm;
        selectContainer.appendChild(progressTypeSelect);
        container.appendChild(selectContainer);
    }

    // Model Usage Tracking
    function incrementUsageForModel(modelId) {
        // Ensure daily counts are reset if a new day has started using latest data usage.
        usageData = Storage.get();
        checkAndResetDaily();

        if (!usageData.models[modelId]) {
            console.debug(
                `[monitor] No mapping found for model "${modelId}". Creating new entry.`
      );
            usageData.models[modelId] = {
                displayName: modelId,
                count: 0,
                dailyLimit: 0,
                lastUpdate: "",
            };
        }

        const model = usageData.models[modelId];
        model.count += 1;
        model.lastUpdate = new Date().toLocaleTimeString();

        if (model.dailyLimit > 0 && model.count > model.dailyLimit) {
            console.debug(`[monitor] Daily limit exceeded for model ${model.displayName}`);
        }

        Storage.set(usageData);
        updateUI();
    }

    // Toast notification function
    function showToast(message, type = 'success') {
        const container = document.getElementById('chatUsageMonitor');

        // Remove any existing toast
        const existingToast = container.querySelector('.toast');
        if (existingToast) {
            existingToast.remove();
        }

        // Create new toast
        const toast = document.createElement('div');
        toast.className = 'toast';
        toast.textContent = message;

        // Set color based on type
        if (type === 'error') {
            toast.style.color = COLORS.danger;
            toast.style.borderColor = COLORS.danger;
        } else if (type === 'warning') {
            toast.style.color = COLORS.warning;
            toast.style.borderColor = COLORS.warning;
        }

        container.appendChild(toast);

        // Show toast
        setTimeout(() => {
            toast.classList.add('show');
        }, 10);

        // Hide toast after 3 seconds
        setTimeout(() => {
            toast.classList.remove('show');
            setTimeout(() => {
                toast.remove();
            }, 300);
        }, 3000);
    }

    // Daily Reset Check
    function checkAndResetDaily() {
        if (usageData.lastReset !== getToday()) {
            Object.values(usageData.models).forEach((model) => {
                model.count = 0;
                model.lastUpdate = "";
            });
            usageData.lastReset = getToday();
            Storage.set(usageData);
        }
    }

    class Draggable {
        constructor(element) {
            this.element = element;
            this.isDragging = false;
            this.initialX = 0;
            this.initialY = 0;
            this.boundHandleMove = this.handleMove.bind(this);
            this.boundHandleEnd = this.handleEnd.bind(this);
            this.init();
        }

        init() {
            const handle = this.element.querySelector('.drag-handle');
            handle.addEventListener('mousedown', this.handleStart.bind(this));
        }

        handleStart(e) {
            this.isDragging = true;
            this.initialX = e.clientX - this.element.offsetLeft;
            this.initialY = e.clientY - this.element.offsetTop;

            document.addEventListener('mousemove', this.boundHandleMove);
            document.addEventListener('mouseup', this.boundHandleEnd);
            requestAnimationFrame(() => this.updatePosition());
        }

        handleMove(e) {
            if (!this.isDragging) return;

            this.currentX = e.clientX - this.initialX;
            this.currentY = e.clientY - this.initialY;
            this.applyBoundaryConstraints();
        }

        applyBoundaryConstraints() {
            const rect = this.element.getBoundingClientRect();
            const maxX = window.innerWidth - rect.width;
            const maxY = window.innerHeight - rect.height;

            this.currentX = Math.min(Math.max(0, this.currentX), maxX);
            this.currentY = Math.min(Math.max(0, this.currentY), maxY);
        }

        updatePosition() {
            if (!this.isDragging) return;

            this.element.style.left = `${this.currentX}px`;
            this.element.style.top = `${this.currentY}px`;
            requestAnimationFrame(() => this.updatePosition());
        }

        handleEnd() {
            this.isDragging = false;
            document.removeEventListener('mousemove', this.boundHandleMove);
            document.removeEventListener('mouseup', this.boundHandleEnd);

            Storage.update(data => {
                data.position = {
                    x: this.currentX,
                    y: this.currentY
                };
            });
        }
    }
    let draggable;
    // UI Creation
    function createMonitorUI() {
        if (document.getElementById("chatUsageMonitor")) return;

        const container = document.createElement("div");
        container.id = "chatUsageMonitor";

        // Make container draggable
        container.style.cursor = "move";

        // Create header with icon tabs
        const header = document.createElement("header");
        const dragHandle = document.createElement("div");
        dragHandle.className = "drag-handle";
        header.appendChild(dragHandle);

        // Set initial position
        if (usageData.position.x !== null && usageData.position.y !== null) {
            // Ensure position is within viewport
            const maxX = window.innerWidth - 360; // container width
            const maxY = window.innerHeight - 500; // container max-height
            container.style.right = "auto";
            container.style.bottom = "auto";
            container.style.left = `${Math.min(
                Math.max(0, usageData.position.x),
                maxX
            )}px`;
            container.style.top = `${Math.min(
                Math.max(0, usageData.position.y),
                maxY
            )}px`;
        } else {
            // bottom-right by default
            container.style.right = STYLE.spacing.lg;
            container.style.bottom = STYLE.spacing.lg;
            container.style.left = "auto";
            container.style.top = "auto";
        }

        const usageTabBtn = document.createElement("button");
        usageTabBtn.innerHTML = `<span>Usage</span>`;
        usageTabBtn.classList.add("active");

        const settingsTabBtn = document.createElement("button");
        settingsTabBtn.innerHTML = `<span>Settings</span>`;

        header.appendChild(usageTabBtn);
        header.appendChild(settingsTabBtn);
        container.appendChild(header);

        container.style.cursor = "default";


        // Create content panels
        const usageContent = document.createElement("div");
        usageContent.className = "content";
        usageContent.id = "usageContent";
        container.appendChild(usageContent);

        const settingsContent = document.createElement("div");
        settingsContent.className = "content";
        settingsContent.id = "settingsContent";
        settingsContent.style.display = "none";
        container.appendChild(settingsContent);

        // Add tab switching logic
        usageTabBtn.addEventListener("click", () => {
            usageTabBtn.classList.add("active");
            settingsTabBtn.classList.remove("active");
            usageContent.style.display = "";
            settingsContent.style.display = "none";
        });

        settingsTabBtn.addEventListener("click", () => {
            settingsTabBtn.classList.add("active");
            usageTabBtn.classList.remove("active");
            settingsContent.style.display = "";
            usageContent.style.display = "none";
        });

        document.body.appendChild(container);
        draggable = new Draggable(container);
        console.debug("[monitor] create ui");
        console.debug("[monitor] draggable", draggable);
        updateUI();
    }

    // Fetch Interception
    const target_window =
          typeof unsafeWindow === "undefined" ? window : unsafeWindow;
    const originalFetch = target_window.fetch;

    target_window.fetch = new Proxy(originalFetch, {
        apply: async function (target, thisArg, args) {
            const response = await target.apply(thisArg, args);

            try {
                const [requestInfo, requestInit] = args;
                const fetchUrl =
                      typeof requestInfo === "string" ? requestInfo : requestInfo?.href;

                if (
                    requestInit?.method === "POST" &&
                    fetchUrl?.endsWith("/conversation")
                ) {
                    const bodyText = requestInit.body;
                    const bodyObj = JSON.parse(bodyText);

                    if (bodyObj?.model) {
                        console.debug("[monitor] Detected model usage:", bodyObj.model);
                        incrementUsageForModel(bodyObj.model);
                    }
                }
            } catch (error) {
                console.warn("[monitor] Failed to process request:", error);
            }

            return response;
        },
    });

    // Initialization
    function initialize() {
        checkAndResetDaily();
        createMonitorUI();
    }

    // Setup Observers and Event Listeners
    if (document.readyState === "loading") {
        target_window.addEventListener("DOMContentLoaded", initialize);
    } else {
        initialize();
    }

    // Observer for dynamic content changes
    const observer = new MutationObserver(() => {
        if (!document.getElementById("chatUsageMonitor")) {
            initialize();
        }
    });

    observer.observe(document.documentElement || document.body, {
        childList: true,
        subtree: true,
    });

    // Handle navigation events
    window.addEventListener("popstate", initialize);
})();

已改,成品

2 Likes

3lue佬真是行动力Max呀 :heart_eyes:

1 Like

啊哈哈哈主要是我自己挺需要这个,不开源的不太敢用

1 Like