PageSourceSearch

https://www.govarena.ai/js/votestore.js

js govarena.ai collected 2026-10-05 12:39:51 UTC 6,015 bytes, 222 lines download raw bytes

1// Gov Arena — Vote Store & Elo Engine
2// localStorage-backed, designed for future Supabase migration
3
4window.VoteStore = (function() {
5  'use strict';
6
7  var VOTES_KEY = 'gov-arena-votes';
8  var ELO_KEY = 'gov-arena-elo';
9  var DEFAULT_ELO = 1200;
10  var K_FACTOR = 32;
11  var MIN_VOTES_THRESHOLD = 5;
12
13  // ==================== STORAGE ====================
14
15  function loadVotes() {
16    try {
17      return JSON.parse(localStorage.getItem(VOTES_KEY)) || [];
18    } catch(e) {
19      return [];
20    }
21  }
22
23  function saveVotes(votes) {
24    localStorage.setItem(VOTES_KEY, JSON.stringify(votes));
25  }
26
27  function loadElo() {
28    try {
29      return JSON.parse(localStorage.getItem(ELO_KEY)) || {};
30    } catch(e) {
31      return {};
32    }
33  }
34
35  function saveElo(elo) {
36    localStorage.setItem(ELO_KEY, JSON.stringify(elo));
37  }
38
39  // ==================== ELO CALCULATION ====================
40
41  function computeEloDelta(ratingA, ratingB, scoreA, kOverride) {
42    // scoreA: 1 = A wins, 0 = A loses, 0.5 = tie
43    var k = kOverride || K_FACTOR;
44    var expectedA = 1.0 / (1.0 + Math.pow(10, (ratingB - ratingA) / 400));
45    return Math.round(k * (scoreA - expectedA));
46  }
47
48  function getModelCatRecord(elo, modelName, catId) {
49    if (!elo[modelName]) elo[modelName] = {};
50    if (!elo[modelName][catId]) elo[modelName][catId] = { rating: DEFAULT_ELO, wins: 0, losses: 0, ties: 0 };
51    return elo[modelName][catId];
52  }
53
54  // ==================== PUBLIC API ====================
55
56  function recordVote(voteRecord) {
57    var votes = loadVotes();
58    var elo = loadElo();
59
60    // Generate vote ID
61    var id = 'v_' + Date.now() + '_' + Math.random().toString(16).slice(2, 6);
62    voteRecord.id = id;
63    voteRecord.timestamp = new Date().toISOString();
64
65    // Truncate question for storage
66    if (voteRecord.question && voteRecord.question.length > 120) {
67      voteRecord.question = voteRecord.question.slice(0, 120) + '...';
68    }
69
70    // Compute Elo deltas (skip if flagged as speed-click)
71    var deltaA = 0;
72    var deltaB = 0;
73
74    if (!voteRecord.flagged) {
75      var catId = voteRecord.categoryId;
76      var recA = getModelCatRecord(elo, voteRecord.modelA, catId);
77      var recB = getModelCatRecord(elo, voteRecord.modelB, catId);
78
79      var scoreA;
80      var kOverride = null;
81      if (voteRecord.winner === 'a') {
82        scoreA = 1;
83        recA.wins++;
84        recB.losses++;
85      } else if (voteRecord.winner === 'b') {
86        scoreA = 0;
87        recA.losses++;
88        recB.wins++;
89      } else if (voteRecord.winner === 'both-good' || voteRecord.winner === 'both-bad') {
90        scoreA = 0.5;
91        recA.ties++;
92        recB.ties++;
93        kOverride = K_FACTOR / 2; // Half impact for both-good/both-bad
94      } else {
95        scoreA = 0.5;
96        recA.ties++;
97        recB.ties++;
98      }
99
100      deltaA = computeEloDelta(recA.rating, recB.rating, scoreA, kOverride);
101      deltaB = -deltaA;
102
103      recA.rating += deltaA;
104      recB.rating += deltaB;
105
106      saveElo(elo);
107    }
108
109    voteRecord.eloDeltaA = deltaA;
110    voteRecord.eloDeltaB = deltaB;
111
112    votes.push(voteRecord);
113    saveVotes(votes);
114
115    return { deltaA: deltaA, deltaB: deltaB };
116  }
117
118  function getElo(modelName, categoryId) {
119    var elo = loadElo();
120    if (elo[modelName] && elo[modelName][categoryId]) {
121      return elo[modelName][categoryId].rating;
122    }
123    return DEFAULT_ELO;
124  }
125
126  function getOverallElo(modelName) {
127    var elo = loadElo();
128    if (!elo[modelName]) return DEFAULT_ELO;
129
130    var cats = Object.keys(elo[modelName]);
131    if (cats.length === 0) return DEFAULT_ELO;
132
133    var sum = 0;
134    for (var i = 0; i < cats.length; i++) {
135      sum += elo[modelName][cats[i]].rating;
136    }
137    // For categories without votes, use default Elo
138    var allCatCount = ALL_CATS.length;
139    var votedSum = sum;
140    var unvotedCount = allCatCount - cats.length;
141    var total = votedSum + (unvotedCount * DEFAULT_ELO);
142    return Math.round(total / allCatCount);
143  }
144
145  function getVoteCount(modelName, categoryId) {
146    var elo = loadElo();
147    if (elo[modelName] && elo[modelName][categoryId]) {
148      var rec = elo[modelName][categoryId];
149      return rec.wins + rec.losses + rec.ties;
150    }
151    return 0;
152  }
153
154  function getTotalVoteCount(modelName) {
155    var elo = loadElo();
156    if (!elo[modelName]) return 0;
157    var total = 0;
158    var cats = Object.keys(elo[modelName]);
159    for (var i = 0; i < cats.length; i++) {
160      var rec = elo[modelName][cats[i]];
161      total += rec.wins + rec.losses + rec.ties;
162    }
163    return total;
164  }
165
166  function getLeaderboard(catId) {
167    if (catId === 'overall') {
168      return SAMPLE_MODELS.map(function(m) {
169        return {
170          name: m.name,
171          short: m.short,
172          elo: getOverallElo(m.name),
173          provider: m.provider,
174          org: m.org,
175          license: m.license,
176          votes: getTotalVoteCount(m.name),
177          provisional: getTotalVoteCount(m.name) < MIN_VOTES_THRESHOLD
178        };
179      }).sort(function(a, b) { return b.elo - a.elo; });
180    }
181
182    return SAMPLE_MODELS.map(function(m) {
183      return {
184        name: m.name,
185        short: m.short,
186        elo: getElo(m.name, catId),
187        provider: m.provider,
188        org: m.org,
189        license: m.license,
190        votes: getVoteCount(m.name, catId),
191        provisional: getVoteCount(m.name, catId) < MIN_VOTES_THRESHOLD
192      };
193    }).sort(function(a, b) { return b.elo - a.elo; });
194  }
195
196  function hasVotes() {
197    var votes = loadVotes();
198    return votes.length > 0;
199  }
200
201  function totalVotes() {
202    return loadVotes().length;
203  }
204
205  function reset() {
206    localStorage.removeItem(VOTES_KEY);
207    localStorage.removeItem(ELO_KEY);
208  }
209
210  return {
211    recordVote: recordVote,
212    getElo: getElo,
213    getOverallElo: getOverallElo,
214    getVoteCount: getVoteCount,
215    getTotalVoteCount: getTotalVoteCount,
216    getLeaderboard: getLeaderboard,
217    hasVotes: hasVotes,
218    totalVotes: totalVotes,
219    reset: reset,
220    MIN_VOTES_THRESHOLD: MIN_VOTES_THRESHOLD,
221  };
222})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.