GameSentenceMiner 2.18.14__py3-none-any.whl → 2.18.16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of GameSentenceMiner might be problematic. Click here for more details.
- GameSentenceMiner/anki.py +8 -53
- GameSentenceMiner/obs.py +1 -2
- GameSentenceMiner/ui/anki_confirmation.py +16 -2
- GameSentenceMiner/util/db.py +11 -7
- GameSentenceMiner/util/games_table.py +320 -0
- GameSentenceMiner/vad.py +3 -3
- GameSentenceMiner/web/anki_api_endpoints.py +506 -0
- GameSentenceMiner/web/database_api.py +239 -117
- GameSentenceMiner/web/static/css/loading-skeleton.css +41 -0
- GameSentenceMiner/web/static/css/search.css +54 -0
- GameSentenceMiner/web/static/css/stats.css +76 -0
- GameSentenceMiner/web/static/js/anki_stats.js +304 -50
- GameSentenceMiner/web/static/js/database.js +44 -7
- GameSentenceMiner/web/static/js/heatmap.js +326 -0
- GameSentenceMiner/web/static/js/overview.js +20 -224
- GameSentenceMiner/web/static/js/search.js +190 -23
- GameSentenceMiner/web/static/js/stats.js +371 -1
- GameSentenceMiner/web/stats.py +188 -0
- GameSentenceMiner/web/templates/anki_stats.html +145 -58
- GameSentenceMiner/web/templates/components/date-range.html +19 -0
- GameSentenceMiner/web/templates/components/html-head.html +45 -0
- GameSentenceMiner/web/templates/components/js-config.html +37 -0
- GameSentenceMiner/web/templates/components/popups.html +15 -0
- GameSentenceMiner/web/templates/components/settings-modal.html +233 -0
- GameSentenceMiner/web/templates/database.html +13 -3
- GameSentenceMiner/web/templates/goals.html +9 -31
- GameSentenceMiner/web/templates/overview.html +16 -223
- GameSentenceMiner/web/templates/search.html +46 -0
- GameSentenceMiner/web/templates/stats.html +49 -311
- GameSentenceMiner/web/texthooking_page.py +4 -66
- {gamesentenceminer-2.18.14.dist-info → gamesentenceminer-2.18.16.dist-info}/METADATA +1 -1
- {gamesentenceminer-2.18.14.dist-info → gamesentenceminer-2.18.16.dist-info}/RECORD +36 -27
- {gamesentenceminer-2.18.14.dist-info → gamesentenceminer-2.18.16.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.18.14.dist-info → gamesentenceminer-2.18.16.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.18.14.dist-info → gamesentenceminer-2.18.16.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.18.14.dist-info → gamesentenceminer-2.18.16.dist-info}/top_level.txt +0 -0
|
@@ -12,6 +12,27 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
12
12
|
const ankiCoverage = document.getElementById('ankiCoverage');
|
|
13
13
|
const fromDateInput = document.getElementById('fromDate');
|
|
14
14
|
const toDateInput = document.getElementById('toDate');
|
|
15
|
+
const ankiConnectWarning = document.getElementById('ankiConnectWarning');
|
|
16
|
+
|
|
17
|
+
// Function to show/hide AnkiConnect warning
|
|
18
|
+
function showAnkiConnectWarning(show) {
|
|
19
|
+
if (ankiConnectWarning) {
|
|
20
|
+
ankiConnectWarning.style.display = show ? 'block' : 'none';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Initialize heatmap renderer with mining-specific configuration
|
|
25
|
+
const miningHeatmapRenderer = new HeatmapRenderer({
|
|
26
|
+
containerId: 'miningHeatmapContainer',
|
|
27
|
+
metricName: 'sentences',
|
|
28
|
+
metricLabel: 'sentences mined'
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Function to create GitHub-style heatmap for mining activity using shared component
|
|
32
|
+
function createMiningHeatmap(heatmapData) {
|
|
33
|
+
miningHeatmapRenderer.render(heatmapData);
|
|
34
|
+
}
|
|
35
|
+
|
|
15
36
|
|
|
16
37
|
console.log('Found DOM elements:', {
|
|
17
38
|
loading, error, missingKanjiGrid, missingKanjiCount,
|
|
@@ -50,8 +71,13 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
50
71
|
missingKanjiCount
|
|
51
72
|
});
|
|
52
73
|
|
|
53
|
-
|
|
54
|
-
if (
|
|
74
|
+
// Remove loading skeletons and update values
|
|
75
|
+
if (ankiTotalKanji) {
|
|
76
|
+
ankiTotalKanji.innerHTML = data.anki_kanji_count;
|
|
77
|
+
}
|
|
78
|
+
if (gsmTotalKanji) {
|
|
79
|
+
gsmTotalKanji.innerHTML = data.gsm_kanji_count;
|
|
80
|
+
}
|
|
55
81
|
if (ankiCoverage) {
|
|
56
82
|
const gsmCount = Number(data.gsm_kanji_count);
|
|
57
83
|
const missingCount = Array.isArray(data.missing_kanji) ? data.missing_kanji.length : 0;
|
|
@@ -59,34 +85,15 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
59
85
|
if (gsmCount > 0) {
|
|
60
86
|
percent = ((gsmCount - missingCount) / gsmCount) * 100;
|
|
61
87
|
}
|
|
62
|
-
ankiCoverage.
|
|
88
|
+
ankiCoverage.innerHTML = percent.toFixed(1) + '%';
|
|
89
|
+
}
|
|
90
|
+
if (missingKanjiCount) {
|
|
91
|
+
const missingCount = Array.isArray(data.missing_kanji) ? data.missing_kanji.length : 0;
|
|
92
|
+
missingKanjiCount.innerHTML = missingCount;
|
|
63
93
|
}
|
|
64
94
|
renderKanjiGrid(data.missing_kanji);
|
|
65
95
|
}
|
|
66
96
|
|
|
67
|
-
async function loadStats(start_timestamp = null, end_timestamp = null) {
|
|
68
|
-
console.log('Loading Anki stats...');
|
|
69
|
-
showLoading(true);
|
|
70
|
-
showError(false);
|
|
71
|
-
try {
|
|
72
|
-
// Build URL with optional query params
|
|
73
|
-
const params = new URLSearchParams();
|
|
74
|
-
if (start_timestamp) params.append('start_timestamp', start_timestamp);
|
|
75
|
-
if (end_timestamp) params.append('end_timestamp', end_timestamp);
|
|
76
|
-
const url = '/api/anki_stats' + (params.toString() ? `?${params.toString()}` : '');
|
|
77
|
-
|
|
78
|
-
const resp = await fetch(url);
|
|
79
|
-
if (!resp.ok) throw new Error('Failed to load');
|
|
80
|
-
const data = await resp.json();
|
|
81
|
-
console.log('Received data:', data);
|
|
82
|
-
updateStats(data);
|
|
83
|
-
} catch (e) {
|
|
84
|
-
console.error('Failed to load Anki stats:', e);
|
|
85
|
-
showError(true);
|
|
86
|
-
} finally {
|
|
87
|
-
showLoading(false);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
97
|
|
|
91
98
|
function getUnixTimestampsInMilliseconds(startDate, endDate) {
|
|
92
99
|
// Parse the start date and create a Date object at the beginning of the day
|
|
@@ -100,42 +107,172 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
100
107
|
return { startTimestamp, endTimestamp };
|
|
101
108
|
}
|
|
102
109
|
|
|
110
|
+
// Progressive data loading function - loads each section independently
|
|
111
|
+
async function loadAllStats(start_timestamp = null, end_timestamp = null) {
|
|
112
|
+
console.log('Loading Anki stats with progressive loading...');
|
|
113
|
+
showLoading(true);
|
|
114
|
+
showError(false);
|
|
115
|
+
|
|
116
|
+
// Show all loading spinners
|
|
117
|
+
const gameStatsLoading = document.getElementById('gameStatsLoading');
|
|
118
|
+
const nsfwSfwRetentionLoading = document.getElementById('nsfwSfwRetentionLoading');
|
|
119
|
+
if (gameStatsLoading) gameStatsLoading.style.display = 'flex';
|
|
120
|
+
if (nsfwSfwRetentionLoading) nsfwSfwRetentionLoading.style.display = 'flex';
|
|
121
|
+
|
|
122
|
+
// Build query parameters
|
|
123
|
+
const params = new URLSearchParams();
|
|
124
|
+
if (start_timestamp) params.append('start_timestamp', start_timestamp);
|
|
125
|
+
if (end_timestamp) params.append('end_timestamp', end_timestamp);
|
|
126
|
+
const queryString = params.toString() ? `?${params.toString()}` : '';
|
|
127
|
+
|
|
128
|
+
// Load sections progressively and concurrently
|
|
129
|
+
const loadPromises = [
|
|
130
|
+
loadKanjiStats(queryString),
|
|
131
|
+
loadGameStats(queryString),
|
|
132
|
+
// loadNsfwSfwRetention(queryString),
|
|
133
|
+
loadMiningHeatmap(queryString)
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
// Wait for all sections to complete
|
|
137
|
+
try {
|
|
138
|
+
await Promise.allSettled(loadPromises);
|
|
139
|
+
showAnkiConnectWarning(false);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
console.error('Some stats failed to load:', e);
|
|
142
|
+
showAnkiConnectWarning(true);
|
|
143
|
+
} finally {
|
|
144
|
+
showLoading(false);
|
|
145
|
+
// Hide loading spinners
|
|
146
|
+
if (gameStatsLoading) gameStatsLoading.style.display = 'none';
|
|
147
|
+
if (nsfwSfwRetentionLoading) nsfwSfwRetentionLoading.style.display = 'none';
|
|
148
|
+
|
|
149
|
+
// Show tables/grids
|
|
150
|
+
const gameStatsTable = document.getElementById('gameStatsTable');
|
|
151
|
+
const nsfwSfwRetentionStats = document.getElementById('nsfwSfwRetentionStats');
|
|
152
|
+
if (gameStatsTable) gameStatsTable.style.display = 'table';
|
|
153
|
+
if (nsfwSfwRetentionStats) nsfwSfwRetentionStats.style.display = 'grid';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Individual loading functions for each section
|
|
158
|
+
async function loadKanjiStats(queryString) {
|
|
159
|
+
try {
|
|
160
|
+
const resp = await fetch(`/api/anki_kanji_stats${queryString}`);
|
|
161
|
+
if (!resp.ok) throw new Error('Failed to load kanji stats');
|
|
162
|
+
const data = await resp.json();
|
|
163
|
+
console.log('Received kanji data:', data);
|
|
164
|
+
updateStats(data);
|
|
165
|
+
} catch (e) {
|
|
166
|
+
console.error('Failed to load kanji stats:', e);
|
|
167
|
+
// Show error in kanji section
|
|
168
|
+
const missingKanjiCount = document.getElementById('missingKanjiCount');
|
|
169
|
+
if (missingKanjiCount) missingKanjiCount.textContent = 'Error';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function loadGameStats(queryString) {
|
|
174
|
+
try {
|
|
175
|
+
const resp = await fetch(`/api/anki_game_stats${queryString}`);
|
|
176
|
+
if (!resp.ok) throw new Error('Failed to load game stats');
|
|
177
|
+
const data = await resp.json();
|
|
178
|
+
console.log('Received game stats data:', data);
|
|
179
|
+
renderGameStatsTable(data);
|
|
180
|
+
} catch (e) {
|
|
181
|
+
console.error('Failed to load game stats:', e);
|
|
182
|
+
const gameStatsEmpty = document.getElementById('gameStatsEmpty');
|
|
183
|
+
if (gameStatsEmpty) {
|
|
184
|
+
gameStatsEmpty.style.display = 'block';
|
|
185
|
+
gameStatsEmpty.textContent = 'Failed to load game statistics. Make sure Anki is running with AnkiConnect.';
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function loadNsfwSfwRetention(queryString) {
|
|
191
|
+
try {
|
|
192
|
+
const resp = await fetch(`/api/anki_nsfw_sfw_retention${queryString}`);
|
|
193
|
+
if (!resp.ok) throw new Error('Failed to load NSFW/SFW retention');
|
|
194
|
+
const data = await resp.json();
|
|
195
|
+
console.log('Received NSFW/SFW retention data:', data);
|
|
196
|
+
renderNsfwSfwRetention(data);
|
|
197
|
+
} catch (e) {
|
|
198
|
+
console.error('Failed to load NSFW/SFW retention:', e);
|
|
199
|
+
const nsfwSfwRetentionEmpty = document.getElementById('nsfwSfwRetentionEmpty');
|
|
200
|
+
if (nsfwSfwRetentionEmpty) {
|
|
201
|
+
nsfwSfwRetentionEmpty.style.display = 'block';
|
|
202
|
+
nsfwSfwRetentionEmpty.textContent = 'Failed to load retention statistics. Make sure Anki is running with AnkiConnect.';
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function loadMiningHeatmap(queryString) {
|
|
208
|
+
try {
|
|
209
|
+
const resp = await fetch(`/api/anki_mining_heatmap${queryString}`);
|
|
210
|
+
if (!resp.ok) throw new Error('Failed to load mining heatmap');
|
|
211
|
+
const data = await resp.json();
|
|
212
|
+
console.log('Received mining heatmap data:', data);
|
|
213
|
+
|
|
214
|
+
if (data && Object.keys(data).length > 0) {
|
|
215
|
+
createMiningHeatmap(data);
|
|
216
|
+
} else {
|
|
217
|
+
const container = document.getElementById('miningHeatmapContainer');
|
|
218
|
+
container.innerHTML = '<p style="text-align: center; color: var(--text-tertiary); padding: 20px;">No mining data available for the selected date range.</p>';
|
|
219
|
+
}
|
|
220
|
+
} catch (e) {
|
|
221
|
+
console.error('Failed to load mining heatmap:', e);
|
|
222
|
+
const container = document.getElementById('miningHeatmapContainer');
|
|
223
|
+
container.innerHTML = '<p style="text-align: center; color: var(--text-tertiary); padding: 20px;">Failed to load mining heatmap.</p>';
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
103
227
|
document.addEventListener("datesSetAnki", () => {
|
|
104
228
|
const fromDate = sessionStorage.getItem("fromDateAnki");
|
|
105
229
|
const toDate = sessionStorage.getItem("toDateAnki");
|
|
106
230
|
const { startTimestamp, endTimestamp } = getUnixTimestampsInMilliseconds(fromDate, toDate);
|
|
107
231
|
|
|
108
|
-
|
|
232
|
+
// Use unified endpoint instead of multiple calls
|
|
233
|
+
loadAllStats(startTimestamp, endTimestamp);
|
|
109
234
|
});
|
|
110
235
|
|
|
111
|
-
function initializeDates() {
|
|
236
|
+
async function initializeDates() {
|
|
112
237
|
const fromDateInput = document.getElementById('fromDate');
|
|
113
238
|
const toDateInput = document.getElementById('toDate');
|
|
114
239
|
|
|
115
240
|
const fromDate = sessionStorage.getItem("fromDateAnki");
|
|
116
|
-
const toDate = sessionStorage.getItem("toDateAnki");
|
|
241
|
+
const toDate = sessionStorage.getItem("toDateAnki");
|
|
117
242
|
|
|
118
243
|
if (!(fromDate && toDate)) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
244
|
+
try {
|
|
245
|
+
// Fetch earliest date from the dedicated endpoint
|
|
246
|
+
const resp = await fetch('/api/anki_earliest_date');
|
|
247
|
+
const data = await resp.json();
|
|
248
|
+
|
|
249
|
+
// Get first date in ms from API
|
|
250
|
+
const firstDateinMs = data.earliest_date;
|
|
251
|
+
const firstDateObject = new Date(firstDateinMs);
|
|
252
|
+
const fromDate = firstDateObject.toLocaleDateString('en-CA');
|
|
253
|
+
fromDateInput.value = fromDate;
|
|
254
|
+
|
|
255
|
+
// Get today's date
|
|
256
|
+
const today = new Date();
|
|
257
|
+
const toDate = today.toLocaleDateString('en-CA');
|
|
258
|
+
toDateInput.value = toDate;
|
|
259
|
+
|
|
260
|
+
// Save in sessionStorage
|
|
261
|
+
sessionStorage.setItem("fromDateAnki", fromDate);
|
|
262
|
+
sessionStorage.setItem("toDateAnki", toDate);
|
|
263
|
+
|
|
264
|
+
document.dispatchEvent(new Event("datesSetAnki"));
|
|
265
|
+
} catch (e) {
|
|
266
|
+
console.error('Failed to initialize dates:', e);
|
|
267
|
+
// Fallback to today if API fails
|
|
268
|
+
const today = new Date();
|
|
269
|
+
const todayStr = today.toLocaleDateString('en-CA');
|
|
270
|
+
fromDateInput.value = todayStr;
|
|
271
|
+
toDateInput.value = todayStr;
|
|
272
|
+
sessionStorage.setItem("fromDateAnki", todayStr);
|
|
273
|
+
sessionStorage.setItem("toDateAnki", todayStr);
|
|
274
|
+
document.dispatchEvent(new Event("datesSetAnki"));
|
|
275
|
+
}
|
|
139
276
|
} else {
|
|
140
277
|
// If values already in sessionStorage, set inputs from there
|
|
141
278
|
fromDateInput.value = fromDate;
|
|
@@ -160,11 +297,128 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
160
297
|
|
|
161
298
|
const { startTimestamp, endTimestamp } = getUnixTimestampsInMilliseconds(fromDateStr, toDateStr);
|
|
162
299
|
|
|
163
|
-
|
|
300
|
+
// Use unified endpoint instead of multiple calls
|
|
301
|
+
loadAllStats(startTimestamp, endTimestamp);
|
|
164
302
|
}
|
|
165
303
|
|
|
166
304
|
fromDateInput.addEventListener("change", handleDateChange);
|
|
167
305
|
toDateInput.addEventListener("change", handleDateChange);
|
|
168
306
|
|
|
169
307
|
initializeDates();
|
|
308
|
+
|
|
309
|
+
function renderGameStatsTable(gameStats) {
|
|
310
|
+
const gameStatsTableBody = document.getElementById('gameStatsTableBody');
|
|
311
|
+
const gameStatsEmpty = document.getElementById('gameStatsEmpty');
|
|
312
|
+
|
|
313
|
+
if (!gameStats || gameStats.length === 0) {
|
|
314
|
+
gameStatsTableBody.innerHTML = '';
|
|
315
|
+
gameStatsEmpty.style.display = 'block';
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
gameStatsEmpty.style.display = 'none';
|
|
320
|
+
|
|
321
|
+
// Clear existing rows
|
|
322
|
+
gameStatsTableBody.innerHTML = '';
|
|
323
|
+
|
|
324
|
+
// Populate table with game stats
|
|
325
|
+
gameStats.forEach(game => {
|
|
326
|
+
const row = document.createElement('tr');
|
|
327
|
+
|
|
328
|
+
// Game name cell
|
|
329
|
+
const nameCell = document.createElement('td');
|
|
330
|
+
nameCell.textContent = game.game_name;
|
|
331
|
+
row.appendChild(nameCell);
|
|
332
|
+
|
|
333
|
+
// Average time per card cell
|
|
334
|
+
const timeCell = document.createElement('td');
|
|
335
|
+
timeCell.textContent = formatTime(game.avg_time_per_card);
|
|
336
|
+
row.appendChild(timeCell);
|
|
337
|
+
|
|
338
|
+
// Retention percentage cell
|
|
339
|
+
const retentionCell = document.createElement('td');
|
|
340
|
+
retentionCell.textContent = game.retention_pct + '%';
|
|
341
|
+
retentionCell.style.color = getRetentionColor(game.retention_pct);
|
|
342
|
+
row.appendChild(retentionCell);
|
|
343
|
+
|
|
344
|
+
gameStatsTableBody.appendChild(row);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function getRetentionColor(retention) {
|
|
349
|
+
if (retention >= 80) {
|
|
350
|
+
return 'var(--success-color, #2ecc71)';
|
|
351
|
+
} else if (retention >= 70) {
|
|
352
|
+
return 'var(--warning-color, #f39c12)';
|
|
353
|
+
} else {
|
|
354
|
+
return 'var(--danger-color, #e74c3c)';
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function formatTime(seconds) {
|
|
359
|
+
if (seconds < 1) {
|
|
360
|
+
return (seconds * 1000).toFixed(0) + 'ms';
|
|
361
|
+
} else if (seconds < 60) {
|
|
362
|
+
return seconds.toFixed(1) + 's';
|
|
363
|
+
} else {
|
|
364
|
+
const minutes = Math.floor(seconds / 60);
|
|
365
|
+
const remainingSeconds = Math.floor(seconds % 60);
|
|
366
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Note: Old individual loading functions (loadGameStats, loadNsfwSfwRetention, loadStats, loadMiningHeatmap)
|
|
371
|
+
// have been replaced by the unified loadAllStats function for better performance
|
|
372
|
+
|
|
373
|
+
function renderNsfwSfwRetention(data) {
|
|
374
|
+
const nsfwRetentionEl = document.getElementById('nsfwRetention');
|
|
375
|
+
const sfwRetentionEl = document.getElementById('sfwRetention');
|
|
376
|
+
const nsfwReviewsEl = document.getElementById('nsfwReviews');
|
|
377
|
+
const sfwReviewsEl = document.getElementById('sfwReviews');
|
|
378
|
+
const nsfwAvgTimeEl = document.getElementById('nsfwAvgTime');
|
|
379
|
+
const sfwAvgTimeEl = document.getElementById('sfwAvgTime');
|
|
380
|
+
const nsfwSfwRetentionEmpty = document.getElementById('nsfwSfwRetentionEmpty');
|
|
381
|
+
|
|
382
|
+
// Check if we have any data
|
|
383
|
+
if (data.nsfw_reviews === 0 && data.sfw_reviews === 0) {
|
|
384
|
+
nsfwSfwRetentionEmpty.style.display = 'block';
|
|
385
|
+
document.getElementById('nsfwSfwRetentionStats').style.display = 'none';
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
nsfwSfwRetentionEmpty.style.display = 'none';
|
|
390
|
+
|
|
391
|
+
// Update NSFW retention (remove skeleton and set content)
|
|
392
|
+
if (data.nsfw_reviews > 0) {
|
|
393
|
+
nsfwRetentionEl.innerHTML = data.nsfw_retention + '%';
|
|
394
|
+
nsfwRetentionEl.style.color = getRetentionColor(data.nsfw_retention);
|
|
395
|
+
nsfwReviewsEl.textContent = data.nsfw_reviews + ' reviews';
|
|
396
|
+
nsfwAvgTimeEl.innerHTML = formatTime(data.nsfw_avg_time);
|
|
397
|
+
nsfwAvgTimeEl.style.color = 'var(--text-primary)';
|
|
398
|
+
} else {
|
|
399
|
+
nsfwRetentionEl.innerHTML = 'N/A';
|
|
400
|
+
nsfwRetentionEl.style.color = 'var(--text-tertiary)';
|
|
401
|
+
nsfwReviewsEl.textContent = 'No reviews';
|
|
402
|
+
nsfwAvgTimeEl.innerHTML = 'N/A';
|
|
403
|
+
nsfwAvgTimeEl.style.color = 'var(--text-tertiary)';
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Update SFW retention (remove skeleton and set content)
|
|
407
|
+
if (data.sfw_reviews > 0) {
|
|
408
|
+
sfwRetentionEl.innerHTML = data.sfw_retention + '%';
|
|
409
|
+
sfwRetentionEl.style.color = getRetentionColor(data.sfw_retention);
|
|
410
|
+
sfwReviewsEl.textContent = data.sfw_reviews + ' reviews';
|
|
411
|
+
sfwAvgTimeEl.innerHTML = formatTime(data.sfw_avg_time);
|
|
412
|
+
sfwAvgTimeEl.style.color = 'var(--text-primary)';
|
|
413
|
+
} else {
|
|
414
|
+
sfwRetentionEl.innerHTML = 'N/A';
|
|
415
|
+
sfwRetentionEl.style.color = 'var(--text-tertiary)';
|
|
416
|
+
sfwReviewsEl.textContent = 'No reviews';
|
|
417
|
+
sfwAvgTimeEl.innerHTML = 'N/A';
|
|
418
|
+
sfwAvgTimeEl.style.color = 'var(--text-tertiary)';
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Note: NSFW/SFW retention stats are now loaded via the unified loadAllStats function
|
|
423
|
+
// which is triggered by the "datesSetAnki" event listener above (line 218-225)
|
|
170
424
|
});
|
|
@@ -90,6 +90,12 @@ class DatabaseManager {
|
|
|
90
90
|
if (removeDuplicatesBtn) {
|
|
91
91
|
removeDuplicatesBtn.addEventListener('click', removeDuplicates);
|
|
92
92
|
}
|
|
93
|
+
|
|
94
|
+
// Add event listener for the ignore time window checkbox
|
|
95
|
+
const ignoreTimeWindowCheckbox = document.getElementById('ignoreTimeWindow');
|
|
96
|
+
if (ignoreTimeWindowCheckbox) {
|
|
97
|
+
ignoreTimeWindowCheckbox.addEventListener('change', toggleTimeWindowVisibility);
|
|
98
|
+
}
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
async loadDashboardStats() {
|
|
@@ -444,8 +450,28 @@ async function openDeduplicationModal() {
|
|
|
444
450
|
await loadGamesForDeduplication();
|
|
445
451
|
// Reset modal state
|
|
446
452
|
document.getElementById('timeWindow').value = '5';
|
|
453
|
+
document.getElementById('ignoreTimeWindow').checked = false;
|
|
447
454
|
document.getElementById('deduplicationStats').style.display = 'none';
|
|
448
455
|
document.getElementById('removeDuplicatesBtn').disabled = true;
|
|
456
|
+
document.getElementById('deduplicationError').style.display = 'none';
|
|
457
|
+
document.getElementById('deduplicationSuccess').style.display = 'none';
|
|
458
|
+
// Ensure time window is visible on modal open
|
|
459
|
+
toggleTimeWindowVisibility();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function toggleTimeWindowVisibility() {
|
|
463
|
+
const ignoreTimeWindow = document.getElementById('ignoreTimeWindow').checked;
|
|
464
|
+
const timeWindowGroup = document.getElementById('timeWindowGroup');
|
|
465
|
+
|
|
466
|
+
if (ignoreTimeWindow) {
|
|
467
|
+
timeWindowGroup.style.opacity = '0.5';
|
|
468
|
+
timeWindowGroup.style.pointerEvents = 'none';
|
|
469
|
+
document.getElementById('timeWindow').disabled = true;
|
|
470
|
+
} else {
|
|
471
|
+
timeWindowGroup.style.opacity = '1';
|
|
472
|
+
timeWindowGroup.style.pointerEvents = 'auto';
|
|
473
|
+
document.getElementById('timeWindow').disabled = false;
|
|
474
|
+
}
|
|
449
475
|
}
|
|
450
476
|
|
|
451
477
|
async function loadGamesForDeduplication() {
|
|
@@ -474,6 +500,7 @@ async function scanForDuplicates() {
|
|
|
474
500
|
const selectedGames = Array.from(document.getElementById('gameSelection').selectedOptions).map(option => option.value);
|
|
475
501
|
const timeWindow = parseInt(document.getElementById('timeWindow').value);
|
|
476
502
|
const caseSensitive = document.getElementById('caseSensitiveDedup').checked;
|
|
503
|
+
const ignoreTimeWindow = document.getElementById('ignoreTimeWindow').checked;
|
|
477
504
|
const statsDiv = document.getElementById('deduplicationStats');
|
|
478
505
|
const errorDiv = document.getElementById('deduplicationError');
|
|
479
506
|
const successDiv = document.getElementById('deduplicationSuccess');
|
|
@@ -491,8 +518,9 @@ async function scanForDuplicates() {
|
|
|
491
518
|
return;
|
|
492
519
|
}
|
|
493
520
|
|
|
494
|
-
|
|
495
|
-
|
|
521
|
+
// Only validate time window if not ignoring it
|
|
522
|
+
if (!ignoreTimeWindow && (isNaN(timeWindow) || timeWindow < 1)) {
|
|
523
|
+
errorDiv.textContent = 'Time window must be at least 1 minute';
|
|
496
524
|
errorDiv.style.display = 'block';
|
|
497
525
|
return;
|
|
498
526
|
}
|
|
@@ -502,6 +530,7 @@ async function scanForDuplicates() {
|
|
|
502
530
|
games: selectedGames,
|
|
503
531
|
time_window_minutes: timeWindow,
|
|
504
532
|
case_sensitive: caseSensitive,
|
|
533
|
+
ignore_time_window: ignoreTimeWindow,
|
|
505
534
|
preview_only: true
|
|
506
535
|
};
|
|
507
536
|
|
|
@@ -533,10 +562,12 @@ async function scanForDuplicates() {
|
|
|
533
562
|
removeBtn.disabled = result.duplicates_count === 0;
|
|
534
563
|
|
|
535
564
|
if (result.duplicates_count > 0) {
|
|
536
|
-
|
|
565
|
+
const modeText = ignoreTimeWindow ? 'across entire games' : `within ${timeWindow} minute time window`;
|
|
566
|
+
successDiv.textContent = `Found ${result.duplicates_count} duplicate sentences ${modeText} ready for removal.`;
|
|
537
567
|
successDiv.style.display = 'block';
|
|
538
568
|
} else {
|
|
539
|
-
|
|
569
|
+
const modeText = ignoreTimeWindow ? 'across entire games' : 'within the specified time window';
|
|
570
|
+
successDiv.textContent = `No duplicates found in the selected games ${modeText}.`;
|
|
540
571
|
successDiv.style.display = 'block';
|
|
541
572
|
}
|
|
542
573
|
} else {
|
|
@@ -553,7 +584,8 @@ async function scanForDuplicates() {
|
|
|
553
584
|
|
|
554
585
|
statsDiv.style.display = 'block';
|
|
555
586
|
removeBtn.disabled = false;
|
|
556
|
-
|
|
587
|
+
const modeText = ignoreTimeWindow ? 'across entire games' : 'with time window';
|
|
588
|
+
successDiv.textContent = `Preview feature ready - found ${duplicatesFound} potential duplicates ${modeText} (backend endpoint needed)`;
|
|
557
589
|
successDiv.style.display = 'block';
|
|
558
590
|
}
|
|
559
591
|
}
|
|
@@ -563,8 +595,10 @@ async function removeDuplicates() {
|
|
|
563
595
|
const timeWindow = parseInt(document.getElementById('timeWindow').value);
|
|
564
596
|
const caseSensitive = document.getElementById('caseSensitiveDedup').checked;
|
|
565
597
|
const preserveNewest = document.getElementById('preserveNewest').checked;
|
|
598
|
+
const ignoreTimeWindow = document.getElementById('ignoreTimeWindow').checked;
|
|
566
599
|
|
|
567
|
-
|
|
600
|
+
const modeText = ignoreTimeWindow ? 'ALL duplicate sentences across entire games' : 'duplicate sentences within the time window';
|
|
601
|
+
if (!confirm(`This will permanently remove ${modeText}. Continue?`)) {
|
|
568
602
|
return;
|
|
569
603
|
}
|
|
570
604
|
|
|
@@ -574,6 +608,7 @@ async function removeDuplicates() {
|
|
|
574
608
|
time_window_minutes: timeWindow,
|
|
575
609
|
case_sensitive: caseSensitive,
|
|
576
610
|
preserve_newest: preserveNewest,
|
|
611
|
+
ignore_time_window: ignoreTimeWindow,
|
|
577
612
|
preview_only: false
|
|
578
613
|
};
|
|
579
614
|
|
|
@@ -587,7 +622,8 @@ async function removeDuplicates() {
|
|
|
587
622
|
|
|
588
623
|
if (response.ok) {
|
|
589
624
|
const successDiv = document.getElementById('deduplicationSuccess');
|
|
590
|
-
|
|
625
|
+
const resultModeText = ignoreTimeWindow ? 'across entire games' : `within ${timeWindow} minute time window`;
|
|
626
|
+
successDiv.textContent = `Successfully removed ${result.deleted_count} duplicate sentences ${resultModeText}!`;
|
|
591
627
|
successDiv.style.display = 'block';
|
|
592
628
|
document.getElementById('removeDuplicatesBtn').disabled = true;
|
|
593
629
|
// Refresh dashboard stats
|
|
@@ -607,6 +643,7 @@ async function removeDuplicates() {
|
|
|
607
643
|
}
|
|
608
644
|
}
|
|
609
645
|
|
|
646
|
+
|
|
610
647
|
// Game Merge Functions
|
|
611
648
|
async function openGameMergeModal() {
|
|
612
649
|
const selectedCheckboxes = document.querySelectorAll('.game-checkbox:checked');
|