Claude Usage Summary Search Page v1.2.0
Description for Usage Summary Search Page v1.2.0.
Script Content
// ==UserScript==
// @name Kagi: Usage Summary for Home/Search
// @namespace https://github.com/tjhleeds/user-scripts/
// @version 1.2.0
// @description Display Kagi billing progress on home and search pages
// @author tjhleeds
// @match https://kagi.com/
// @match https://kagi.com/search*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const SEARCHES_PER_MONTH = 300;
const SEARCHES_PER_YEAR = SEARCHES_PER_MONTH * 12;
const CACHE_DURATION_MS = 5 * 60 * 1000; // 5 minutes
const CACHE_KEY = 'kagi_progress_data';
const CACHE_TIMESTAMP_KEY = 'kagi_progress_timestamp';
let progressOverlay = null;
function getCachedData() {
try {
const timestamp = localStorage.getItem(CACHE_TIMESTAMP_KEY);
const data = localStorage.getItem(CACHE_KEY);
if (timestamp && data) {
const age = Date.now() - parseInt(timestamp);
if (age < CACHE_DURATION_MS) {
return JSON.parse(data);
}
}
} catch (error) {
console.error('Error reading cached data:', error);
}
return null;
}
function setCachedData(data) {
try {
localStorage.setItem(CACHE_KEY, JSON.stringify(data));
localStorage.setItem(CACHE_TIMESTAMP_KEY, Date.now().toString());
} catch (error) {
console.error('Error caching data:', error);
}
}
async function fetchBillingData() {
try {
const response = await fetch('https://kagi.com/settings/billing', {
method: 'GET',
credentials: 'same-origin',
headers: {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
}
});
if (!response.ok) {
throw new Error(`Request failed with status: ${response.status}`);
}
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Find renewal date
const renewalElement = Array.from(doc.querySelectorAll('span, div'))
.find(el => el.textContent.includes('Next renewal is'));
if (!renewalElement) {
throw new Error('Renewal element not found');
}
const renewalMatch = renewalElement.textContent.match(/Next renewal is (\d{4}-\d{2}-\d{2})/);
if (!renewalMatch) {
throw new Error('Could not parse renewal date');
}
// Find search count
const searchCountElement = doc.querySelector('.billing_box_count_num_1');
if (!searchCountElement) {
throw new Error('Search count element not found');
}
const searchCount = parseInt(searchCountElement.textContent.trim());
const renewalDate = new Date(renewalMatch[1]);
return { searchCount, renewalDate };
} catch (error) {
console.error('Error fetching billing data:', error);
throw error;
}
}
function calculateProgress(searchCount, renewalDate) {
const currentDate = new Date();
const yearAgo = new Date(renewalDate);
yearAgo.setFullYear(yearAgo.getFullYear() - 1);
// Calculate time-based progress
const totalYearMs = renewalDate.getTime() - yearAgo.getTime();
const elapsedYearMs = currentDate.getTime() - yearAgo.getTime();
const yearProgress = Math.min(100, Math.max(0, (elapsedYearMs / totalYearMs) * 100));
// Calculate expected searches based on year progress
const expectedSearches = (yearProgress / 100) * SEARCHES_PER_YEAR;
// Calculate search progress
const searchProgress = Math.min(100, (searchCount / SEARCHES_PER_YEAR) * 100);
// Calculate search usage rate
const searchRate = expectedSearches > 0 ? (searchCount / expectedSearches) * 100 : 0;
const difference = searchCount - expectedSearches;
return { yearProgress, searchProgress, searchRate, searchCount, expectedSearches, difference };
}
function createProgressOverlay(data) {
if (progressOverlay) {
progressOverlay.remove();
}
progressOverlay = document.createElement('div');
progressOverlay.style.cssText = `
position: fixed;
top: 20px;
left: 20px;
z-index: 10000;
border: 1px solid rgb(163 158 52);
background-color: rgb(57 58 17);
padding: 12px;
border-radius: 6px;
color: rgb(204 204 204);
font-size: 14px;
font-family: system-ui, -apple-system, sans-serif;
max-width: 300px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
transition: opacity 0.5s ease-in-out;
`;
const creationTime = Date.now();
let fadeTimeoutId = null;
const fadeFn = () => {
progressOverlay.style.opacity = '0.2';
};
fadeTimeoutId = setTimeout(fadeFn, 3000);
progressOverlay.addEventListener('mouseenter', () => {
clearTimeout(fadeTimeoutId);
progressOverlay.style.opacity = '1';
});
progressOverlay.addEventListener('mouseleave', () => {
const elapsed = Date.now() - creationTime;
if (elapsed >= 3000) {
progressOverlay.style.opacity = '0.2';
} else {
fadeTimeoutId = setTimeout(fadeFn, 3000 - elapsed);
}
});
const closeButton = document.createElement('button');
closeButton.innerHTML = '×';
closeButton.style.cssText = `
position: absolute;
top: 4px;
right: 8px;
background: none;
border: none;
color: rgb(204 204 204);
font-size: 18px;
cursor: pointer;
padding: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
`;
closeButton.onclick = () => progressOverlay.remove();
const contentDiv = document.createElement('div');
contentDiv.style.paddingRight = '20px';
const difference = data.difference;
const differenceText = difference >= 0
? `${Math.round(difference)} above expected`
: `${Math.round(Math.abs(difference))} below expected`;
contentDiv.innerHTML = `
Year progress: ${data.yearProgress.toFixed(1)}% (${Math.round(data.expectedSearches)}/${SEARCHES_PER_YEAR})
Search progress: ${data.searchProgress.toFixed(1)}% (${data.searchCount}/${SEARCHES_PER_YEAR})
Search rate: ${data.searchRate.toFixed(1)}% of expected usage (${differenceText})
`;
progressOverlay.appendChild(closeButton);
progressOverlay.appendChild(contentDiv);
document.body.appendChild(progressOverlay);
}
async function showProgressInfo() {
try {
// Try cached data first
let cachedData = getCachedData();
if (cachedData && cachedData.hasOwnProperty('difference')) {
createProgressOverlay(cachedData);
return;
}
// Fetch fresh data
const billingData = await fetchBillingData();
const progressData = calculateProgress(billingData.searchCount, billingData.renewalDate);
setCachedData(progressData);
createProgressOverlay(progressData);
} catch (error) {
console.error('Error showing progress info:', error);
}
}
// Initialize when page loads
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', showProgressInfo);
} else {
showProgressInfo();
}
})();