champ-chatbot / static /components /carbon-tracker-component.js
qyle's picture
Deploy from GitLab 2a1446b1
b1b5dda
Raw
History Blame Contribute Delete
9.35 kB
// 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 = `
<td data-label="${translations[StateManager.currentLang]["emissions_model"]}: ">${this.modelNames[modelType] || modelType}</td>
<td data-label="${translations[StateManager.currentLang]["emissions_co2"]}: ">${this.formatEmissions(emissions)}</td>
<td data-label="${translations[StateManager.currentLang]["emissions_tokens"]}: ">${this.formatNumber(tokens)}</td>
<td data-label="${translations[StateManager.currentLang]["emissions_replies"]}: ">${this.formatNumber(replies)}</td>
<td data-label="${translations[StateManager.currentLang]["emissions_per_token"]}: ">${emissionsPerToken > 0 ? this.formatEmissions(emissionsPerToken) : '—'}</td>
<td data-label="${translations[StateManager.currentLang]["emissions_water"]}: ">${this.formatWater(water)}</td>
<td data-label="${translations[StateManager.currentLang]["emissions_electricity"]}: ">${this.formatElectricity(electricity)}</td>
`;
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';
},
};