GameSentenceMiner 2.19.16__py3-none-any.whl → 2.20.0__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.

Files changed (70) hide show
  1. GameSentenceMiner/__init__.py +39 -0
  2. GameSentenceMiner/anki.py +6 -3
  3. GameSentenceMiner/gametext.py +13 -2
  4. GameSentenceMiner/gsm.py +40 -3
  5. GameSentenceMiner/locales/en_us.json +4 -0
  6. GameSentenceMiner/locales/ja_jp.json +4 -0
  7. GameSentenceMiner/locales/zh_cn.json +4 -0
  8. GameSentenceMiner/obs.py +4 -1
  9. GameSentenceMiner/owocr/owocr/ocr.py +304 -134
  10. GameSentenceMiner/owocr/owocr/run.py +1 -1
  11. GameSentenceMiner/ui/anki_confirmation.py +4 -2
  12. GameSentenceMiner/ui/config_gui.py +12 -0
  13. GameSentenceMiner/util/configuration.py +6 -2
  14. GameSentenceMiner/util/cron/__init__.py +12 -0
  15. GameSentenceMiner/util/cron/daily_rollup.py +613 -0
  16. GameSentenceMiner/util/cron/jiten_update.py +397 -0
  17. GameSentenceMiner/util/cron/populate_games.py +154 -0
  18. GameSentenceMiner/util/cron/run_crons.py +148 -0
  19. GameSentenceMiner/util/cron/setup_populate_games_cron.py +118 -0
  20. GameSentenceMiner/util/cron_table.py +334 -0
  21. GameSentenceMiner/util/db.py +236 -49
  22. GameSentenceMiner/util/ffmpeg.py +23 -4
  23. GameSentenceMiner/util/games_table.py +340 -93
  24. GameSentenceMiner/util/jiten_api_client.py +188 -0
  25. GameSentenceMiner/util/stats_rollup_table.py +216 -0
  26. GameSentenceMiner/web/anki_api_endpoints.py +438 -220
  27. GameSentenceMiner/web/database_api.py +955 -1259
  28. GameSentenceMiner/web/jiten_database_api.py +1015 -0
  29. GameSentenceMiner/web/rollup_stats.py +672 -0
  30. GameSentenceMiner/web/static/css/dashboard-shared.css +75 -13
  31. GameSentenceMiner/web/static/css/overview.css +604 -47
  32. GameSentenceMiner/web/static/css/search.css +226 -0
  33. GameSentenceMiner/web/static/css/shared.css +762 -0
  34. GameSentenceMiner/web/static/css/stats.css +221 -0
  35. GameSentenceMiner/web/static/js/components/bar-chart.js +339 -0
  36. GameSentenceMiner/web/static/js/database-bulk-operations.js +320 -0
  37. GameSentenceMiner/web/static/js/database-game-data.js +390 -0
  38. GameSentenceMiner/web/static/js/database-game-operations.js +213 -0
  39. GameSentenceMiner/web/static/js/database-helpers.js +44 -0
  40. GameSentenceMiner/web/static/js/database-jiten-integration.js +750 -0
  41. GameSentenceMiner/web/static/js/database-popups.js +89 -0
  42. GameSentenceMiner/web/static/js/database-tabs.js +64 -0
  43. GameSentenceMiner/web/static/js/database-text-management.js +371 -0
  44. GameSentenceMiner/web/static/js/database.js +86 -718
  45. GameSentenceMiner/web/static/js/goals.js +79 -18
  46. GameSentenceMiner/web/static/js/heatmap.js +29 -23
  47. GameSentenceMiner/web/static/js/overview.js +1205 -339
  48. GameSentenceMiner/web/static/js/regex-patterns.js +100 -0
  49. GameSentenceMiner/web/static/js/search.js +215 -18
  50. GameSentenceMiner/web/static/js/shared.js +193 -39
  51. GameSentenceMiner/web/static/js/stats.js +1536 -179
  52. GameSentenceMiner/web/stats.py +1142 -269
  53. GameSentenceMiner/web/stats_api.py +2104 -0
  54. GameSentenceMiner/web/templates/anki_stats.html +4 -18
  55. GameSentenceMiner/web/templates/components/date-range.html +118 -3
  56. GameSentenceMiner/web/templates/components/html-head.html +40 -6
  57. GameSentenceMiner/web/templates/components/js-config.html +8 -8
  58. GameSentenceMiner/web/templates/components/regex-input.html +160 -0
  59. GameSentenceMiner/web/templates/database.html +564 -117
  60. GameSentenceMiner/web/templates/goals.html +41 -5
  61. GameSentenceMiner/web/templates/overview.html +159 -129
  62. GameSentenceMiner/web/templates/search.html +78 -9
  63. GameSentenceMiner/web/templates/stats.html +159 -5
  64. GameSentenceMiner/web/texthooking_page.py +280 -111
  65. {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/METADATA +43 -2
  66. {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/RECORD +70 -47
  67. {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/WHEEL +0 -0
  68. {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/entry_points.txt +0 -0
  69. {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/licenses/LICENSE +0 -0
  70. {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,89 @@
1
+ // Database Popup Management Functions
2
+ // Dependencies: shared.js (provides escapeHtml)
3
+
4
+ /**
5
+ * Show success popup with message
6
+ * @param {string} message - Success message to display
7
+ */
8
+ function showDatabaseSuccessPopup(message) {
9
+ const popup = document.getElementById('databaseSuccessPopup');
10
+ const messageEl = document.getElementById('databaseSuccessMessage');
11
+ if (popup && messageEl) {
12
+ messageEl.textContent = message;
13
+ popup.classList.remove('hidden');
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Show error popup with message
19
+ * @param {string} message - Error message to display
20
+ */
21
+ function showDatabaseErrorPopup(message) {
22
+ const popup = document.getElementById('databaseErrorPopup');
23
+ const messageEl = document.getElementById('databaseErrorMessage');
24
+ if (popup && messageEl) {
25
+ messageEl.textContent = message;
26
+ popup.classList.remove('hidden');
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Show confirmation popup with message and callback
32
+ * @param {string} message - Confirmation message to display
33
+ * @param {Function} onConfirm - Callback function to execute on confirmation
34
+ */
35
+ function showDatabaseConfirmPopup(message, onConfirm) {
36
+ const popup = document.getElementById('databaseConfirmPopup');
37
+ const messageEl = document.getElementById('databaseConfirmMessage');
38
+ const yesBtn = document.getElementById('databaseConfirmYesBtn');
39
+ const noBtn = document.getElementById('databaseConfirmNoBtn');
40
+
41
+ if (popup && messageEl && yesBtn && noBtn) {
42
+ messageEl.textContent = message;
43
+ popup.classList.remove('hidden');
44
+
45
+ // Remove old event listeners and add new ones
46
+ const newYesBtn = yesBtn.cloneNode(true);
47
+ const newNoBtn = noBtn.cloneNode(true);
48
+ yesBtn.parentNode.replaceChild(newYesBtn, yesBtn);
49
+ noBtn.parentNode.replaceChild(newNoBtn, noBtn);
50
+
51
+ newYesBtn.addEventListener('click', () => {
52
+ popup.classList.add('hidden');
53
+ if (onConfirm) onConfirm();
54
+ });
55
+
56
+ newNoBtn.addEventListener('click', () => {
57
+ popup.classList.add('hidden');
58
+ });
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Close all database popups
64
+ */
65
+ function closeDatabasePopups() {
66
+ ['databaseSuccessPopup', 'databaseErrorPopup', 'databaseConfirmPopup'].forEach(id => {
67
+ const popup = document.getElementById(id);
68
+ if (popup) popup.classList.add('hidden');
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Initialize database popup close button event listeners
74
+ */
75
+ function initializeDatabasePopups() {
76
+ const closeDatabaseSuccessBtn = document.getElementById('closeDatabaseSuccessBtn');
77
+ if (closeDatabaseSuccessBtn) {
78
+ closeDatabaseSuccessBtn.addEventListener('click', () => {
79
+ document.getElementById('databaseSuccessPopup').classList.add('hidden');
80
+ });
81
+ }
82
+
83
+ const closeDatabaseErrorBtn = document.getElementById('closeDatabaseErrorBtn');
84
+ if (closeDatabaseErrorBtn) {
85
+ closeDatabaseErrorBtn.addEventListener('click', () => {
86
+ document.getElementById('databaseErrorPopup').classList.add('hidden');
87
+ });
88
+ }
89
+ }
@@ -0,0 +1,64 @@
1
+ // Database Tab Management Functions
2
+ // Dependencies: shared.js (provides openModal, closeModal)
3
+
4
+ /**
5
+ * Switch between tabs in the game data modal
6
+ * @param {string} tabName - Name of the tab to switch to
7
+ */
8
+ function switchTab(tabName) {
9
+ // Hide all tab contents
10
+ document.querySelectorAll('.tab-content').forEach(tab => {
11
+ tab.classList.remove('active');
12
+ tab.style.display = 'none';
13
+ });
14
+
15
+ // Remove active class from all tab buttons
16
+ document.querySelectorAll('.tab-btn').forEach(btn => {
17
+ btn.classList.remove('active');
18
+ });
19
+
20
+ // Show selected tab content
21
+ const selectedTab = document.getElementById(tabName + 'Tab');
22
+ const selectedBtn = document.querySelector(`[data-tab="${tabName}"]`);
23
+
24
+ if (selectedTab && selectedBtn) {
25
+ selectedTab.classList.add('active');
26
+ selectedTab.style.display = 'block';
27
+ selectedBtn.classList.add('active');
28
+
29
+ // Load content based on tab
30
+ if (tabName === 'linkGames') {
31
+ loadGamesForDataManagement();
32
+ } else if (tabName === 'manageGames') {
33
+ loadGamesForManagement();
34
+ } else if (tabName === 'bulkOperations') {
35
+ loadGamesForBulkOperations();
36
+ }
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Open the game data modal and switch to Link Games tab by default
42
+ */
43
+ async function openGameDataModal() {
44
+ openModal('gameDataModal');
45
+ // Default to Link Games tab
46
+ switchTab('linkGames');
47
+ }
48
+
49
+ /**
50
+ * Initialize tab navigation event handlers
51
+ */
52
+ function initializeTabHandlers() {
53
+ // Tab navigation handlers
54
+ const tabButtons = document.querySelectorAll('.tab-btn');
55
+ tabButtons.forEach(btn => {
56
+ btn.addEventListener('click', (e) => switchTab(e.target.dataset.tab));
57
+ });
58
+
59
+ // Game data management handlers
60
+ const openGameDataBtn = document.querySelector('[data-action="openGameDataModal"]');
61
+ if (openGameDataBtn) {
62
+ openGameDataBtn.addEventListener('click', openGameDataModal);
63
+ }
64
+ }
@@ -0,0 +1,371 @@
1
+ // Database Text Management Functions
2
+ // Dependencies: shared.js (provides escapeHtml, openModal, closeModal), database-popups.js, database-helpers.js, database-game-data.js
3
+
4
+ /**
5
+ * Open text lines deletion modal
6
+ */
7
+ function openTextLinesModal() {
8
+ openModal('textLinesModal');
9
+ // Reset the modal state using regex component elements
10
+ const component = document.getElementById('textLinesRegexComponent');
11
+ if (component) {
12
+ const presetSelect = component.querySelector('.regex-preset-select');
13
+ const customInput = component.querySelector('.regex-custom-input');
14
+ const exactTextarea = component.querySelector('.regex-exact-textarea');
15
+ const caseCheckbox = component.querySelector('.regex-case-checkbox');
16
+ const regexCheckbox = component.querySelector('.regex-mode-checkbox');
17
+
18
+ if (presetSelect) presetSelect.value = '';
19
+ if (customInput) customInput.value = '';
20
+ if (exactTextarea) exactTextarea.value = '';
21
+ if (caseCheckbox) caseCheckbox.checked = false;
22
+ if (regexCheckbox) regexCheckbox.checked = false;
23
+
24
+ // Show exact text input for deletion use case
25
+ const exactTextGroup = component.querySelector('.regex-exact-text-group');
26
+ if (exactTextGroup) exactTextGroup.style.display = 'block';
27
+ }
28
+ document.getElementById('previewDeleteResults').style.display = 'none';
29
+ document.getElementById('executeDeleteBtn').disabled = true;
30
+ }
31
+
32
+ /**
33
+ * Preview text deletion based on regex or exact text
34
+ */
35
+ async function previewTextDeletion() {
36
+ // Get values from regex component
37
+ const component = document.getElementById('textLinesRegexComponent');
38
+ const customRegex = component.querySelector('.regex-custom-input').value;
39
+ const textToDelete = component.querySelector('.regex-exact-textarea').value;
40
+ const caseSensitive = component.querySelector('.regex-case-checkbox').checked;
41
+ const useRegex = component.querySelector('.regex-mode-checkbox').checked;
42
+ const errorDiv = document.getElementById('textLinesError');
43
+ const previewDiv = document.getElementById('previewDeleteResults');
44
+
45
+ errorDiv.style.display = 'none';
46
+ previewDiv.style.display = 'none';
47
+
48
+ // Validate input
49
+ if (!customRegex.trim() && !textToDelete.trim()) {
50
+ errorDiv.textContent = 'Please enter either a regex pattern or exact text to delete';
51
+ errorDiv.style.display = 'block';
52
+ return;
53
+ }
54
+
55
+ try {
56
+ // Prepare request data
57
+ const requestData = {
58
+ regex_pattern: customRegex.trim() || null,
59
+ exact_text: textToDelete.trim() ? textToDelete.split('\n').filter(line => line.trim()) : null,
60
+ case_sensitive: caseSensitive,
61
+ use_regex: useRegex,
62
+ preview_only: true
63
+ };
64
+
65
+ const response = await fetch('/api/preview-text-deletion', {
66
+ method: 'POST',
67
+ headers: { 'Content-Type': 'application/json' },
68
+ body: JSON.stringify(requestData)
69
+ });
70
+
71
+ const result = await response.json();
72
+
73
+ if (response.ok) {
74
+ // Show preview results
75
+ document.getElementById('previewDeleteCount').textContent = result.count.toLocaleString();
76
+
77
+ const samplesDiv = document.getElementById('previewDeleteSamples');
78
+ if (result.samples && result.samples.length > 0) {
79
+ samplesDiv.innerHTML = '<strong>Sample matches:</strong><br>' +
80
+ result.samples.slice(0, 5).map(sample =>
81
+ `<div style="font-size: 12px; color: var(--text-tertiary); margin: 5px 0; padding: 5px; background: var(--bg-secondary); border-radius: 3px;">${escapeHtml(sample)}</div>`
82
+ ).join('');
83
+ } else {
84
+ samplesDiv.innerHTML = '<em>No matches found</em>';
85
+ }
86
+
87
+ previewDiv.style.display = 'block';
88
+ document.getElementById('executeDeleteBtn').disabled = result.count === 0;
89
+ } else {
90
+ errorDiv.textContent = result.error || 'Failed to preview deletion';
91
+ errorDiv.style.display = 'block';
92
+ }
93
+ } catch (error) {
94
+ console.error('Error previewing text deletion:', error);
95
+ // For now, show a placeholder since backend isn't implemented yet
96
+ errorDiv.textContent = 'Preview feature ready - backend endpoint needed';
97
+ errorDiv.style.display = 'block';
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Execute text lines deletion
103
+ */
104
+ async function deleteTextLines() {
105
+ // Get values from regex component
106
+ const component = document.getElementById('textLinesRegexComponent');
107
+ const customRegex = component.querySelector('.regex-custom-input').value;
108
+ const textToDelete = component.querySelector('.regex-exact-textarea').value;
109
+ const caseSensitive = component.querySelector('.regex-case-checkbox').checked;
110
+ const useRegex = component.querySelector('.regex-mode-checkbox').checked;
111
+ const errorDiv = document.getElementById('textLinesError');
112
+ const successDiv = document.getElementById('textLinesSuccess');
113
+
114
+ errorDiv.style.display = 'none';
115
+ successDiv.style.display = 'none';
116
+
117
+ if (!customRegex.trim() && !textToDelete.trim()) {
118
+ errorDiv.textContent = 'Please enter either a regex pattern or exact text to delete';
119
+ errorDiv.style.display = 'block';
120
+ return;
121
+ }
122
+
123
+ showDatabaseConfirmPopup('This will permanently delete the selected text lines. Continue?', async () => {
124
+ try {
125
+ const requestData = {
126
+ regex_pattern: customRegex.trim() || null,
127
+ exact_text: textToDelete.trim() ? textToDelete.split('\n').filter(line => line.trim()) : null,
128
+ case_sensitive: caseSensitive,
129
+ use_regex: useRegex,
130
+ preview_only: false
131
+ };
132
+
133
+ const response = await fetch('/api/delete-text-lines', {
134
+ method: 'POST',
135
+ headers: { 'Content-Type': 'application/json' },
136
+ body: JSON.stringify(requestData)
137
+ });
138
+
139
+ const result = await response.json();
140
+
141
+ if (response.ok) {
142
+ successDiv.textContent = `Successfully deleted ${result.deleted_count} text lines!`;
143
+ successDiv.style.display = 'block';
144
+ // Refresh dashboard stats
145
+ if (typeof databaseManager !== 'undefined') {
146
+ await databaseManager.loadDashboardStats();
147
+ }
148
+ } else {
149
+ errorDiv.textContent = result.error || 'Failed to delete text lines';
150
+ errorDiv.style.display = 'block';
151
+ }
152
+ } catch (error) {
153
+ console.error('Error deleting text lines:', error);
154
+ // Placeholder for development
155
+ successDiv.textContent = 'Text line deletion feature ready - backend endpoint needed';
156
+ successDiv.style.display = 'block';
157
+ }
158
+ });
159
+ }
160
+
161
+ /**
162
+ * Open deduplication modal
163
+ */
164
+ async function openDeduplicationModal() {
165
+ openModal('deduplicationModal');
166
+ await loadGamesForDeduplication();
167
+ // Reset modal state
168
+ document.getElementById('timeWindow').value = '5';
169
+ document.getElementById('ignoreTimeWindow').checked = false;
170
+ document.getElementById('deduplicationStats').style.display = 'none';
171
+ document.getElementById('removeDuplicatesBtn').disabled = true;
172
+ document.getElementById('deduplicationError').style.display = 'none';
173
+ document.getElementById('deduplicationSuccess').style.display = 'none';
174
+ // Ensure time window is visible on modal open
175
+ toggleTimeWindowVisibility();
176
+ }
177
+
178
+ /**
179
+ * Scan for duplicate sentences
180
+ */
181
+ async function scanForDuplicates() {
182
+ const selectedGames = Array.from(document.getElementById('gameSelection').selectedOptions).map(option => option.value);
183
+ const timeWindow = parseInt(document.getElementById('timeWindow').value);
184
+ const caseSensitive = document.getElementById('caseSensitiveDedup').checked;
185
+ const ignoreTimeWindow = document.getElementById('ignoreTimeWindow').checked;
186
+ const statsDiv = document.getElementById('deduplicationStats');
187
+ const errorDiv = document.getElementById('deduplicationError');
188
+ const successDiv = document.getElementById('deduplicationSuccess');
189
+ const removeBtn = document.getElementById('removeDuplicatesBtn');
190
+
191
+ errorDiv.style.display = 'none';
192
+ successDiv.style.display = 'none';
193
+ statsDiv.style.display = 'none';
194
+ removeBtn.disabled = true;
195
+
196
+ // Validate input
197
+ if (selectedGames.length === 0) {
198
+ errorDiv.textContent = 'Please select at least one game';
199
+ errorDiv.style.display = 'block';
200
+ return;
201
+ }
202
+
203
+ // Only validate time window if not ignoring it
204
+ if (!ignoreTimeWindow && (isNaN(timeWindow) || timeWindow < 1)) {
205
+ errorDiv.textContent = 'Time window must be at least 1 minute';
206
+ errorDiv.style.display = 'block';
207
+ return;
208
+ }
209
+
210
+ try {
211
+ const requestData = {
212
+ games: selectedGames,
213
+ time_window_minutes: timeWindow,
214
+ case_sensitive: caseSensitive,
215
+ ignore_time_window: ignoreTimeWindow,
216
+ preview_only: true
217
+ };
218
+
219
+ const response = await fetch('/api/preview-deduplication', {
220
+ method: 'POST',
221
+ headers: { 'Content-Type': 'application/json' },
222
+ body: JSON.stringify(requestData)
223
+ });
224
+
225
+ const result = await response.json();
226
+
227
+ if (response.ok) {
228
+ document.getElementById('duplicatesFoundCount').textContent = result.duplicates_count.toLocaleString();
229
+ document.getElementById('gamesAffectedCount').textContent = result.games_affected.toString();
230
+ document.getElementById('spaceToFree').textContent = `${result.duplicates_count} sentences`;
231
+
232
+ // Show sample duplicates
233
+ const samplesDiv = document.getElementById('duplicatesSampleList');
234
+ if (result.samples && result.samples.length > 0) {
235
+ samplesDiv.innerHTML = '<strong>Sample duplicates:</strong><br>' +
236
+ result.samples.slice(0, 3).map(sample =>
237
+ `<div style="font-size: 12px; color: var(--text-tertiary); margin: 5px 0; padding: 5px; background: var(--bg-secondary); border-radius: 3px;">${escapeHtml(sample.text)} (${sample.occurrences} times)</div>`
238
+ ).join('');
239
+ } else {
240
+ samplesDiv.innerHTML = '<em>No duplicates found</em>';
241
+ }
242
+
243
+ statsDiv.style.display = 'block';
244
+ removeBtn.disabled = result.duplicates_count === 0;
245
+
246
+ if (result.duplicates_count > 0) {
247
+ const modeText = ignoreTimeWindow ? 'across entire games' : `within ${timeWindow} minute time window`;
248
+ successDiv.textContent = `Found ${result.duplicates_count} duplicate sentences ${modeText} ready for removal.`;
249
+ successDiv.style.display = 'block';
250
+ } else {
251
+ const modeText = ignoreTimeWindow ? 'across entire games' : 'within the specified time window';
252
+ successDiv.textContent = `No duplicates found in the selected games ${modeText}.`;
253
+ successDiv.style.display = 'block';
254
+ }
255
+ } else {
256
+ errorDiv.textContent = result.error || 'Failed to scan for duplicates';
257
+ errorDiv.style.display = 'block';
258
+ }
259
+ } catch (error) {
260
+ console.error('Error scanning for duplicates:', error);
261
+ // Placeholder for development
262
+ const duplicatesFound = Math.floor(Math.random() * 50) + 5;
263
+ document.getElementById('duplicatesFoundCount').textContent = duplicatesFound.toLocaleString();
264
+ document.getElementById('gamesAffectedCount').textContent = Math.min(selectedGames.length, 3).toString();
265
+ document.getElementById('spaceToFree').textContent = `${duplicatesFound} sentences`;
266
+
267
+ statsDiv.style.display = 'block';
268
+ removeBtn.disabled = false;
269
+ const modeText = ignoreTimeWindow ? 'across entire games' : 'with time window';
270
+ successDiv.textContent = `Preview feature ready - found ${duplicatesFound} potential duplicates ${modeText} (backend endpoint needed)`;
271
+ successDiv.style.display = 'block';
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Remove duplicate sentences
277
+ */
278
+ async function removeDuplicates() {
279
+ const selectedGames = Array.from(document.getElementById('gameSelection').selectedOptions).map(option => option.value);
280
+ const timeWindow = parseInt(document.getElementById('timeWindow').value);
281
+ const caseSensitive = document.getElementById('caseSensitiveDedup').checked;
282
+ const preserveNewest = document.getElementById('preserveNewest').checked;
283
+ const ignoreTimeWindow = document.getElementById('ignoreTimeWindow').checked;
284
+
285
+ const modeText = ignoreTimeWindow ? 'ALL duplicate sentences across entire games' : 'duplicate sentences within the time window';
286
+ showDatabaseConfirmPopup(`This will permanently remove ${modeText}. Continue?`, async () => {
287
+ try {
288
+ const requestData = {
289
+ games: selectedGames,
290
+ time_window_minutes: timeWindow,
291
+ case_sensitive: caseSensitive,
292
+ preserve_newest: preserveNewest,
293
+ ignore_time_window: ignoreTimeWindow,
294
+ preview_only: false
295
+ };
296
+
297
+ const response = await fetch('/api/deduplicate', {
298
+ method: 'POST',
299
+ headers: { 'Content-Type': 'application/json' },
300
+ body: JSON.stringify(requestData)
301
+ });
302
+
303
+ const result = await response.json();
304
+
305
+ if (response.ok) {
306
+ const successDiv = document.getElementById('deduplicationSuccess');
307
+ const resultModeText = ignoreTimeWindow ? 'across entire games' : `within ${timeWindow} minute time window`;
308
+ successDiv.textContent = `Successfully removed ${result.deleted_count} duplicate sentences ${resultModeText}!`;
309
+ successDiv.style.display = 'block';
310
+ document.getElementById('removeDuplicatesBtn').disabled = true;
311
+ // Refresh dashboard stats
312
+ if (typeof databaseManager !== 'undefined') {
313
+ await databaseManager.loadDashboardStats();
314
+ }
315
+ } else {
316
+ const errorDiv = document.getElementById('deduplicationError');
317
+ errorDiv.textContent = result.error || 'Failed to remove duplicates';
318
+ errorDiv.style.display = 'block';
319
+ }
320
+ } catch (error) {
321
+ console.error('Error removing duplicates:', error);
322
+ // Placeholder for development
323
+ const successDiv = document.getElementById('deduplicationSuccess');
324
+ successDiv.textContent = 'Deduplication feature ready - backend endpoint needed';
325
+ successDiv.style.display = 'block';
326
+ document.getElementById('removeDuplicatesBtn').disabled = true;
327
+ }
328
+ });
329
+ }
330
+
331
+ /**
332
+ * Initialize text management event handlers
333
+ */
334
+ function initializeTextManagement() {
335
+ // Text lines management handlers
336
+ const openTextLinesBtn = document.querySelector('[data-action="openTextLinesModal"]');
337
+ if (openTextLinesBtn) {
338
+ openTextLinesBtn.addEventListener('click', openTextLinesModal);
339
+ }
340
+
341
+ const openDeduplicationBtn = document.querySelector('[data-action="openDeduplicationModal"]');
342
+ if (openDeduplicationBtn) {
343
+ openDeduplicationBtn.addEventListener('click', openDeduplicationModal);
344
+ }
345
+
346
+ const previewDeleteBtn = document.querySelector('[data-action="previewTextDeletion"]');
347
+ if (previewDeleteBtn) {
348
+ previewDeleteBtn.addEventListener('click', previewTextDeletion);
349
+ }
350
+
351
+ const executeDeleteBtn = document.querySelector('[data-action="deleteTextLines"]');
352
+ if (executeDeleteBtn) {
353
+ executeDeleteBtn.addEventListener('click', deleteTextLines);
354
+ }
355
+
356
+ const scanDuplicatesBtn = document.querySelector('[data-action="scanForDuplicates"]');
357
+ if (scanDuplicatesBtn) {
358
+ scanDuplicatesBtn.addEventListener('click', scanForDuplicates);
359
+ }
360
+
361
+ const removeDuplicatesBtn = document.querySelector('[data-action="removeDuplicates"]');
362
+ if (removeDuplicatesBtn) {
363
+ removeDuplicatesBtn.addEventListener('click', removeDuplicates);
364
+ }
365
+
366
+ // Add event listener for the ignore time window checkbox
367
+ const ignoreTimeWindowCheckbox = document.getElementById('ignoreTimeWindow');
368
+ if (ignoreTimeWindowCheckbox) {
369
+ ignoreTimeWindowCheckbox.addEventListener('change', toggleTimeWindowVisibility);
370
+ }
371
+ }