// 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, totalWaterCell: null, totalElectricityCell: null, }, // Model display names modelNames: { "champ": "CHAMP (RAG)", "skills_wiki": "CHAMP (wiki)", // Commented for the Promptathon // "champ": "CHAMP_v1", // "qwen": "CHAMP_v2", "openai": "GPT-5.3", "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.elements.totalWaterCell = document.getElementById('totalWater'); this.elements.totalElectricityCell = document.getElementById('totalElectricity'); 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 water value for display * @param {number} water - The water value in L * @returns {string} Formatted string with appropriate unit */ formatWater(water) { if (water < 1) { return `${(water * 1000).toFixed(3)} ml`; } else { return `${water.toFixed(3)} L`; } }, /** * Format electricity value for display * @param {number} electricity - The emissions value in kWh * @returns {string} Formatted string with appropriate unit */ formatElectricity(electricity) { if (electricity < 0.001) { return `${(electricity * 1000000).toFixed(3)} mWh`; } else if (electricity < 1) { return `${(electricity * 1000).toFixed(3)} Wh`; } else { return `${electricity.toFixed(3)} kWh`; } }, /** * 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; let totalWater = 0; let totalElectricity = 0; // Populate each model row Object.keys(StateManager.modelChats).forEach(modelType => { const chat = StateManager.modelChats[modelType]; const emissions = StateManager.getTotalEmissions(modelType); const water = StateManager.getTotalWater(modelType); const electricity = StateManager.getTotalElectricity(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; totalWater += water; totalElectricity += electricity; const row = document.createElement('tr'); // data-label is necessary for mobile. The mobile version of the app masks the table headers and // transforms the table rows into cards in gwp.css. Translation is not possible within a css file, // so we translate here and the css extracts the text from the data-labels to create the content. row.innerHTML = ` ${this.modelNames[modelType] || modelType} ${this.formatEmissions(emissions)} ${this.formatNumber(tokens)} ${this.formatNumber(replies)} ${emissionsPerToken > 0 ? this.formatEmissions(emissionsPerToken) : '—'} ${this.formatWater(water)} ${this.formatElectricity(electricity)} `; this.elements.emissionsTableBody.appendChild(row); }); // Update totals this.elements.totalEmissionsCell.textContent = this.formatEmissions(totalEmissions); this.elements.totalEmissionsCell.setAttribute('data-label', `${translations[StateManager.currentLang]["emissions_co2"]}: `); this.elements.totalTokensCell.textContent = this.formatNumber(totalTokens); this.elements.totalTokensCell.setAttribute('data-label', `${translations[StateManager.currentLang]["emissions_tokens"]}: `); this.elements.totalRepliesCell.textContent = this.formatNumber(totalReplies); this.elements.totalRepliesCell.setAttribute('data-label', `${translations[StateManager.currentLang]["emissions_replies"]}: `); this.elements.totalWaterCell.textContent = this.formatWater(totalWater); this.elements.totalWaterCell.setAttribute('data-label', `${translations[StateManager.currentLang]["emissions_water"]}: `); this.elements.totalElectricityCell.textContent = this.formatElectricity(totalElectricity); this.elements.totalElectricityCell.setAttribute('data-label', `${translations[StateManager.currentLang]["emissions_electricity"]}: `); // Update equivalents this.updateEquivalents(totalEmissions); }, /** * Update the equivalent statistics * @param {number} kgCO2 - Total emissions in kgCO2eq */ updateEquivalents(kgCO2) { // Conversion factors (approximate) const CARROT_MEALS_PER_KG = 1 / 0.04 // 0.04 kgCO2 / 100g const PAPERSHEET_PER_KG = 1 / (5 / 1000) // 5 gCO2 / papersheet const CAR_M_PER_KG = 1 / (0.2 / 1000); // 0.2 kgCO2/km 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 carrotMeals = kgCO2 * CARROT_MEALS_PER_KG; const papersheets = kgCO2 * PAPERSHEET_PER_KG; const carM = kgCO2 * CAR_M_PER_KG; const sms = kgCO2 * SMS_PER_KG; const spam = kgCO2 * SPAM_PER_KG; document.getElementById('carrot').textContent = carrotMeals.toFixed(1); document.getElementById('papersheet').textContent = papersheets.toFixed(1); document.getElementById('carM').textContent = carM.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'; }, };