// components/carbon-tracker.js - Track carbon emissions per model import { StateManager } from '../services/state-manager.js'; import { TranslationService } from '../services/translation-service.js'; export const CarbonTracker = { elements: { toolbarEmissions: null, moreInfoBtn: null, emissionsModal: null, okModalBtn: null, closeModalBtn: null, emissionsTableBody: null, totalEmissionsCell: null, totalTokensCell: null, totalRepliesCell: null, }, // Model display names modelNames: { "champ": "CHAMP_v1", "qwen": "CHAMP_v2", "openai": "GPT-5.2", "google-conservative": translations[StateManager.currentLang]["gemini_conservative"], "google-creative": translations[StateManager.currentLang]["gemini_creative"], }, /** * Initialize the carbon tracker */ init() { this.elements.toolbarEmissions = document.getElementById('toolbar-emissions'); this.elements.moreInfoBtn = document.getElementById('emissions-more-info'); this.elements.emissionsModal = document.getElementById('emissions-modal'); this.elements.okModalBtn = document.getElementById('ok-emissions-btn'); this.elements.closeModalBtn = document.getElementById('close-emissions-btn'); this.elements.emissionsTableBody = document.getElementById('emissions-table-body'); this.elements.totalEmissionsCell = document.getElementById('totalEmissions'); this.elements.totalTokensCell = document.getElementById('totalTokens'); this.elements.totalRepliesCell = document.getElementById('totalReplies'); this.attachEventListeners(); }, attachEventListeners() { this.elements.moreInfoBtn.addEventListener('click', () => this.openModal()); this.elements.okModalBtn.addEventListener('click', () => this.closeModal()); this.elements.closeModalBtn.addEventListener('click', () => this.closeModal()); this.elements.emissionsModal.addEventListener('click', (e) => { if (e.target === this.elements.emissionsModal) { this.closeModal(); } }); }, /** * Format emissions value for display * @param {number} kgCO2eq - The emissions value in kgCO2eq * @returns {string} Formatted string with appropriate unit */ formatEmissions(kgCO2eq) { if (kgCO2eq < 0.001) { return `${(kgCO2eq * 1000000).toFixed(3)} mg`; } else if (kgCO2eq < 1) { return `${(kgCO2eq * 1000).toFixed(3)} g`; } else { return `${kgCO2eq.toFixed(3)} kg`; } }, /** * Format number with thousands separator * @param {number} num - Number to format * @returns {string} Formatted number */ formatNumber(num) { return num.toLocaleString(); }, countReplies(messages) { return messages.filter(msg => msg.role === 'assistant' && msg.content && msg.content !== 'no_reply' ).length; }, /** * Count tokens in messages * @param {Array} messages - Array of message objects * @returns {number} Total token count */ countTokens(messages) { return messages.reduce((total, msg) => { return total + (msg.nTokens || 0); }, 0); }, /** * Update the toolbar emissions display */ updateEmissions() { const totalEmissions = StateManager.getAllEmissions(); this.elements.toolbarEmissions.innerHTML = this.formatEmissions(totalEmissions); }, /** * Populate the emissions table */ populateTable() { this.elements.emissionsTableBody.innerHTML = ''; let totalEmissions = 0; let totalTokens = 0; let totalReplies = 0; // Populate each model row Object.keys(StateManager.modelChats).forEach(modelType => { const chat = StateManager.modelChats[modelType]; const emissions = StateManager.getTotalEmissions(modelType); const tokens = this.countTokens(chat.messages); const replies = this.countReplies(chat.messages); const emissionsPerToken = tokens > 0 ? emissions / tokens : 0; totalEmissions += emissions; totalTokens += tokens; totalReplies += replies; const row = document.createElement('tr'); row.innerHTML = ` ${this.modelNames[modelType] || modelType} ${this.formatEmissions(emissions)} ${this.formatNumber(tokens)} ${this.formatNumber(replies)} ${emissionsPerToken > 0 ? this.formatEmissions(emissionsPerToken) : '—'} `; this.elements.emissionsTableBody.appendChild(row); }); // Update totals this.elements.totalEmissionsCell.textContent = this.formatEmissions(totalEmissions); this.elements.totalTokensCell.textContent = this.formatNumber(totalTokens); this.elements.totalRepliesCell.textContent = this.formatNumber(totalReplies); // Update equivalents this.updateEquivalents(totalEmissions); }, /** * Update the equivalent statistics * @param {number} kgCO2 - Total emissions in kgCO2eq */ updateEquivalents(kgCO2) { // Conversion factors (approximate) const CAR_KM_PER_KG = 1 / 0.2; // 0.2 kgCO2/km const BEEF_MEALS_PER_KG = 1 / 7; // 7 kgCO2/100g const CARROT_MEALS_PER_KG = 1 / 0.04 // 0.04 kgCO2 / 100g const SMS_PER_KG = 1 / (0.014 / 1000) ; // 0.014 gCO2/SMS const SPAM_PER_KG = 1 / (0.03 / 1000) ; // 0.03 gCO2/spam const carKm = kgCO2 * CAR_KM_PER_KG; const beefMeals = kgCO2 * BEEF_MEALS_PER_KG; const carrotMeals = kgCO2 * CARROT_MEALS_PER_KG; const sms = kgCO2 * SMS_PER_KG; const spam = kgCO2 * SPAM_PER_KG; document.getElementById('carKm').textContent = carKm.toFixed(1); document.getElementById('beefMeals').textContent = beefMeals.toFixed(1); document.getElementById('carrot').textContent = carrotMeals.toFixed(1); document.getElementById('spam').textContent = spam.toFixed(1); document.getElementById('sms').textContent = sms.toFixed(1); }, /** * Open the emissions modal */ openModal() { this.populateTable(); this.elements.emissionsModal.style.display = ''; TranslationService.applyTranslation(); }, /** * Close the emissions modal */ closeModal() { this.elements.emissionsModal.style.display = 'none'; }, };