A token counter for SpicyWriter conversations with warnings to start a new thread to avoid context degradation.
// ==UserScript== // @name Spicy Writer Token Counter // @namespace https://gf.qytechs.cn/en/scripts/553875-spicy-writer-token-counter // @version 1.5.1 // @description A token counter for SpicyWriter conversations with warnings to start a new thread to avoid context degradation. // @author Google Gemini 2.5 Pro // @match https://spicywriter.com/* // @match https://*.spicywriter.com/* // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/o200k_base.min.js // @grant GM_addStyle // @run-at document-end // @license MIT // ==/UserScript== /* global GPTTokenizer_o200k_base */ (function() { 'use strict'; // --- User-configurable Settings --- const SETTINGS = { corner: 'top-right', // 'top-right' or 'bottom-right' verticalMargin: 60, // in pixels horizontalMargin: 15, // in pixels debounceDelay: 300, // in milliseconds }; // --- Configuration for Token Thresholds --- const THRESHOLDS = { green: { key: 'green', limit: 8000, color: '#10b981', tip: "Optimal performance. Conversation is fresh. AI will respond with full accuracy." }, lime: { key: 'lime', limit: 16000, color: '#84cc16', tip: "Good quality. AI is still performing well, but consider starting new conversation soon." }, yellow: { key: 'yellow', limit: 24000, color: '#f59e0b', tip: "Quality may decline. AI might miss some context. Consider starting new conversation." }, orange: { key: 'orange', limit: 32000, color: '#f97316', tip: "Performance declining. AI may give less accurate responses. Start new conversation to restore quality." }, red: { key: 'red', limit: Infinity, color: '#ef4444', tip: "Poor performance zone. Start new conversation now to avoid unexpected problems." } }; // --- Main Script --- const DEBUG_MODE = false; const GLOW_COOLDOWN_MS = 60000; // Cooldown for the glow/tooltip animation // --- State Management --- let totalBaseTokens = 0; let totalClaudeTokens = 0; let mainObserver = null; let overlay = null; let currentStatusKey = null; let lastGlowTimestamp = 0; // [BUG FIX] Timestamp for animation cooldown. let inputHandler = null; // --- Utility Functions --- function debounce(func, delay) { let timeoutId; return function(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(this, args), delay); }; } function getTokenCounts(text) { if (!text || typeof text !== 'string') return { base: 0, claude: 0 }; const base = GPTTokenizer_o200k_base.countTokens(text); // Heuristic: Claude models use ~16% more tokens than OpenAI's o200k for similar text. const claude = base * 1.16; return { base, claude }; } function formatTokenCount(num) { if (num < 1000) { return '~' + (Math.ceil(num / 100) * 100); } const k = num / 1000; return '~' + k.toFixed(k < 10 ? 1 : 0) + 'k'; } function formatForTooltip(num) { if (num < 1000) return String(Math.round(num / 10) * 10); const k = num / 1000; return k.toFixed(1) + 'k'; } // --- Overlay Management --- function createOverlay() { if (document.getElementById('token-counter-overlay')) return; overlay = document.createElement('div'); overlay.id = 'token-counter-overlay'; document.body.appendChild(overlay); overlay.style.top = SETTINGS.corner.startsWith('top-') ? `${SETTINGS.verticalMargin}px` : 'auto'; overlay.style.bottom = SETTINGS.corner.startsWith('bottom-') ? `${SETTINGS.verticalMargin}px` : 'auto'; overlay.style.right = SETTINGS.corner.endsWith('-right') ? `${SETTINGS.horizontalMargin}px` : 'auto'; overlay.dataset.position = SETTINGS.corner; overlay.addEventListener('animationend', (event) => { if (event.animationName === 'subtleGlow') { overlay.classList.remove('glow-warning'); } if (event.animationName === 'fadeInOut') { overlay.classList.remove('show-tooltip-animation'); } }); GM_addStyle(` /* Main Overlay Style */ #token-counter-overlay { position: fixed; color: #ffffff; padding: 5px 10px; border-radius: 7px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; font-size: 12px; font-weight: 500; z-index: 9999; display: none; white-space: nowrap; transition: background-color 0.3s ease; } /* Tooltip Style */ #token-counter-overlay:hover::after, #token-counter-overlay.show-tooltip-animation::after { content: attr(data-tooltip); position: absolute; background-color: #0c0505; border: 1px solid #513737; color: #f9fafb; padding: 10px 14px; border-radius: 6px; font-size: 12px; white-space: pre-wrap; width: 250px; text-align: left; pointer-events: none; z-index: 10000; opacity: 1; } #token-counter-overlay[data-position="top-right"]:hover::after, #token-counter-overlay.show-tooltip-animation[data-position="top-right"]::after { top: calc(100% + 5px); right: 0; } #token-counter-overlay[data-position="bottom-right"]:hover::after, #token-counter-overlay.show-tooltip-animation[data-position="bottom-right"]::after { bottom: calc(100% + 5px); right: 0; } /* --- Keyframe Animations --- */ @keyframes fadeInOut { 0%, 100% { opacity: 0; } 5%, 80% { opacity: 1; } } @keyframes subtleGlow { 0%, 100% { box-shadow: 0 0 4px rgba(255, 255, 255, 0.2); } 50% { box-shadow: 0 0 12px 3px rgba(255, 255, 255, 0.6); } } /* --- Animation Trigger Classes --- */ #token-counter-overlay.show-tooltip-animation::after { animation: fadeInOut 7s ease-in-out forwards; } #token-counter-overlay.glow-warning { animation: subtleGlow 1.5s ease-in-out; } `); } function updateOverlay() { if (!overlay) createOverlay(); const displayTokens = totalClaudeTokens; const formattedTokens = formatTokenCount(displayTokens); if (displayTokens > 50) { let status = THRESHOLDS.red; if (displayTokens < THRESHOLDS.green.limit) status = THRESHOLDS.green; else if (displayTokens < THRESHOLDS.lime.limit) status = THRESHOLDS.lime; else if (displayTokens < THRESHOLDS.yellow.limit) status = THRESHOLDS.yellow; else if (displayTokens < THRESHOLDS.orange.limit) status = THRESHOLDS.orange; const newStatusKey = status.key; if (currentStatusKey && newStatusKey !== currentStatusKey) { const isWarningTransition = (newStatusKey === 'orange' && currentStatusKey === 'yellow') || (newStatusKey === 'red' && currentStatusKey === 'orange'); // [BUG FIX] Check for a warning transition AND ensure the cooldown period has passed. if (isWarningTransition && (Date.now() - lastGlowTimestamp > GLOW_COOLDOWN_MS)) { // Update timestamp immediately to start the cooldown. lastGlowTimestamp = Date.now(); overlay.classList.remove('show-tooltip-animation', 'glow-warning'); setTimeout(() => { overlay.classList.add('show-tooltip-animation'); overlay.classList.add('glow-warning'); }, 50); } } currentStatusKey = newStatusKey; const formattedMin = formatForTooltip(totalBaseTokens); const formattedMax = formatForTooltip(totalClaudeTokens); const detailLine = `~${formattedMin}-${formattedMax} tokens in this conversation.`; const fullTooltip = `${detailLine}\n\n${status.tip}`; overlay.textContent = `${formattedTokens} tokens`; overlay.style.backgroundColor = status.color; overlay.setAttribute('data-tooltip', fullTooltip); overlay.style.display = 'block'; } else { overlay.style.display = 'none'; currentStatusKey = null; } } // --- Core Caching & Calculation Logic --- function recountAllTokens() { totalBaseTokens = 0; totalClaudeTokens = 0; const elements = [ ...document.querySelectorAll('.message-text'), ...document.querySelectorAll('textarea.edit-textarea'), document.querySelector('textarea#message-input') ]; elements.forEach(el => { if (!el) return; const text = el.tagName.toLowerCase() === 'textarea' ? el.value : el.innerText; const counts = getTokenCounts(text); totalBaseTokens += counts.base; totalClaudeTokens += counts.claude; }); updateOverlay(); } const debouncedRecount = debounce(recountAllTokens, SETTINGS.debounceDelay); // --- Mutation Observer --- function getCountableDescendants(nodeList) { const elements = []; nodeList.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { if (node.matches('.message-text, textarea.edit-textarea')) { elements.push(node); } elements.push(...node.querySelectorAll('.message-text, textarea.edit-textarea')); } }); return elements; } function handleMutations(mutations) { let hasRelevantChanges = false; let hasNewEditArea = false; for (const mutation of mutations) { if (mutation.type === 'childList') { const added = getCountableDescendants(mutation.addedNodes); const removed = getCountableDescendants(mutation.removedNodes); if (added.length > 0 || removed.length > 0) { hasRelevantChanges = true; if (added.some(el => el.matches('textarea.edit-textarea'))) { hasNewEditArea = true; } } } } if (hasRelevantChanges && !hasNewEditArea) { // Debounced call for general updates (new messages, deleted messages). debouncedRecount(); } if (hasNewEditArea) { // A new edit textarea was added. The framework needs a moment to populate its value. // Schedule a non-debounced, corrective recount to fix the count accurately. setTimeout(recountAllTokens, 150); } } // --- Main Initialization --- function initialize() { if (DEBUG_MODE) console.log("Spicy Writer Token Counter: Initializing..."); createOverlay(); const chatContainer = document.querySelector('[role="log"]'); const mainInput = document.querySelector('textarea#message-input'); if(!chatContainer || !mainInput) { console.error("Spicy Writer Token Counter: Could not find critical elements."); return; } recountAllTokens(); if (mainObserver) mainObserver.disconnect(); mainObserver = new MutationObserver(handleMutations); mainObserver.observe(chatContainer, { childList: true, subtree: true }); if (inputHandler) { document.body.removeEventListener('input', inputHandler); } inputHandler = (event) => { if (event.target.tagName.toLowerCase() === 'textarea' && (event.target.id === 'message-input' || event.target.classList.contains('edit-textarea'))) { debouncedRecount(); } }; document.body.addEventListener('input', inputHandler); } // --- Startup Logic --- function startup() { const chatContainer = document.querySelector('[role="log"]'); if (chatContainer && document.querySelector('textarea#message-input')) { initialize(); } else { const initialObserver = new MutationObserver(() => { const chatLog = document.querySelector('[role="log"]'); const messageInput = document.querySelector('textarea#message-input'); if (chatLog && messageInput) { if (DEBUG_MODE) console.log("Spicy Writer Token Counter: Chat container found, initializing."); initialize(); initialObserver.disconnect(); } }); initialObserver.observe(document.body, { childList: true, subtree: true }); } } startup(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址