GameSentenceMiner 2.15.11__py3-none-any.whl → 2.15.12__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.
- GameSentenceMiner/web/static/css/kanji-grid.css +107 -0
- GameSentenceMiner/web/static/css/search.css +14 -0
- GameSentenceMiner/web/static/css/shared.css +932 -0
- GameSentenceMiner/web/static/css/stats.css +499 -0
- GameSentenceMiner/web/static/js/anki_stats.js +84 -0
- GameSentenceMiner/web/static/js/database.js +541 -0
- GameSentenceMiner/web/static/js/kanji-grid.js +203 -0
- GameSentenceMiner/web/static/js/search.js +273 -0
- GameSentenceMiner/web/static/js/shared.js +506 -0
- GameSentenceMiner/web/static/js/stats.js +1427 -0
- GameSentenceMiner/web/templates/components/navigation.html +16 -0
- GameSentenceMiner/web/templates/components/theme-styles.html +128 -0
- {gamesentenceminer-2.15.11.dist-info → gamesentenceminer-2.15.12.dist-info}/METADATA +1 -1
- {gamesentenceminer-2.15.11.dist-info → gamesentenceminer-2.15.12.dist-info}/RECORD +18 -6
- {gamesentenceminer-2.15.11.dist-info → gamesentenceminer-2.15.12.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.15.11.dist-info → gamesentenceminer-2.15.12.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.15.11.dist-info → gamesentenceminer-2.15.12.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.15.11.dist-info → gamesentenceminer-2.15.12.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,541 @@
|
|
1
|
+
// Database Management JavaScript
|
2
|
+
// Dependencies: shared.js (provides utility functions like escapeHtml, openModal, closeModal)
|
3
|
+
|
4
|
+
// Database Management Class
|
5
|
+
class DatabaseManager {
|
6
|
+
constructor() {
|
7
|
+
this.selectedGames = new Set();
|
8
|
+
this.initializePage();
|
9
|
+
}
|
10
|
+
|
11
|
+
async initializePage() {
|
12
|
+
await this.loadDashboardStats();
|
13
|
+
this.attachEventHandlers();
|
14
|
+
}
|
15
|
+
|
16
|
+
attachEventHandlers() {
|
17
|
+
// Attach event handlers for buttons that were using onclick
|
18
|
+
const openGameDeletionBtn = document.querySelector('[data-action="openGameDeletionModal"]');
|
19
|
+
if (openGameDeletionBtn) {
|
20
|
+
openGameDeletionBtn.addEventListener('click', openGameDeletionModal);
|
21
|
+
}
|
22
|
+
|
23
|
+
const openTextLinesBtn = document.querySelector('[data-action="openTextLinesModal"]');
|
24
|
+
if (openTextLinesBtn) {
|
25
|
+
openTextLinesBtn.addEventListener('click', openTextLinesModal);
|
26
|
+
}
|
27
|
+
|
28
|
+
const openDeduplicationBtn = document.querySelector('[data-action="openDeduplicationModal"]');
|
29
|
+
if (openDeduplicationBtn) {
|
30
|
+
openDeduplicationBtn.addEventListener('click', openDeduplicationModal);
|
31
|
+
}
|
32
|
+
|
33
|
+
// Modal close handlers
|
34
|
+
const closeButtons = document.querySelectorAll('[data-action="closeModal"]');
|
35
|
+
closeButtons.forEach(btn => {
|
36
|
+
const modalId = btn.getAttribute('data-modal');
|
37
|
+
if (modalId) {
|
38
|
+
btn.addEventListener('click', () => closeModal(modalId));
|
39
|
+
}
|
40
|
+
});
|
41
|
+
|
42
|
+
// Other action buttons
|
43
|
+
const selectAllBtn = document.querySelector('[data-action="selectAllGames"]');
|
44
|
+
if (selectAllBtn) {
|
45
|
+
selectAllBtn.addEventListener('click', selectAllGames);
|
46
|
+
}
|
47
|
+
|
48
|
+
const selectNoneBtn = document.querySelector('[data-action="selectNoGames"]');
|
49
|
+
if (selectNoneBtn) {
|
50
|
+
selectNoneBtn.addEventListener('click', selectNoGames);
|
51
|
+
}
|
52
|
+
|
53
|
+
const deleteSelectedBtn = document.querySelector('[data-action="deleteSelectedGames"]');
|
54
|
+
if (deleteSelectedBtn) {
|
55
|
+
deleteSelectedBtn.addEventListener('click', deleteSelectedGames);
|
56
|
+
}
|
57
|
+
|
58
|
+
const presetPatternsSelect = document.getElementById('presetPatterns');
|
59
|
+
if (presetPatternsSelect) {
|
60
|
+
presetPatternsSelect.addEventListener('change', applyPresetPattern);
|
61
|
+
}
|
62
|
+
|
63
|
+
const previewDeleteBtn = document.querySelector('[data-action="previewTextDeletion"]');
|
64
|
+
if (previewDeleteBtn) {
|
65
|
+
previewDeleteBtn.addEventListener('click', previewTextDeletion);
|
66
|
+
}
|
67
|
+
|
68
|
+
const executeDeleteBtn = document.querySelector('[data-action="deleteTextLines"]');
|
69
|
+
if (executeDeleteBtn) {
|
70
|
+
executeDeleteBtn.addEventListener('click', deleteTextLines);
|
71
|
+
}
|
72
|
+
|
73
|
+
const scanDuplicatesBtn = document.querySelector('[data-action="scanForDuplicates"]');
|
74
|
+
if (scanDuplicatesBtn) {
|
75
|
+
scanDuplicatesBtn.addEventListener('click', scanForDuplicates);
|
76
|
+
}
|
77
|
+
|
78
|
+
const removeDuplicatesBtn = document.querySelector('[data-action="removeDuplicates"]');
|
79
|
+
if (removeDuplicatesBtn) {
|
80
|
+
removeDuplicatesBtn.addEventListener('click', removeDuplicates);
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
async loadDashboardStats() {
|
85
|
+
try {
|
86
|
+
const response = await fetch('/api/games-list');
|
87
|
+
const data = await response.json();
|
88
|
+
|
89
|
+
if (response.ok && data.games) {
|
90
|
+
const totalGames = data.games.length;
|
91
|
+
const totalSentences = data.games.reduce((sum, game) => sum + game.sentence_count, 0);
|
92
|
+
|
93
|
+
document.getElementById('totalGamesCount').textContent = totalGames.toLocaleString();
|
94
|
+
document.getElementById('totalSentencesCount').textContent = totalSentences.toLocaleString();
|
95
|
+
}
|
96
|
+
} catch (error) {
|
97
|
+
console.error('Error loading dashboard stats:', error);
|
98
|
+
document.getElementById('totalGamesCount').textContent = 'Error';
|
99
|
+
document.getElementById('totalSentencesCount').textContent = 'Error';
|
100
|
+
}
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
104
|
+
// Games Management Functions
|
105
|
+
async function openGameDeletionModal() {
|
106
|
+
openModal('gamesDeletionModal');
|
107
|
+
await loadGamesForDeletion();
|
108
|
+
}
|
109
|
+
|
110
|
+
async function loadGamesForDeletion() {
|
111
|
+
const loadingIndicator = document.getElementById('gamesLoadingIndicator');
|
112
|
+
const content = document.getElementById('gamesContent');
|
113
|
+
const gamesList = document.getElementById('gamesList');
|
114
|
+
|
115
|
+
loadingIndicator.style.display = 'flex';
|
116
|
+
content.style.display = 'none';
|
117
|
+
|
118
|
+
try {
|
119
|
+
const response = await fetch('/api/games-list');
|
120
|
+
const data = await response.json();
|
121
|
+
|
122
|
+
if (response.ok && data.games) {
|
123
|
+
gamesList.innerHTML = '';
|
124
|
+
|
125
|
+
data.games.forEach(game => {
|
126
|
+
const gameItem = document.createElement('div');
|
127
|
+
gameItem.className = 'checkbox-container';
|
128
|
+
gameItem.innerHTML = `
|
129
|
+
<input type="checkbox" class="checkbox-input game-checkbox" data-game="${escapeHtml(game.name)}">
|
130
|
+
<label class="checkbox-label">
|
131
|
+
<strong>${escapeHtml(game.name)}</strong><br>
|
132
|
+
<small style="color: var(--text-tertiary);">
|
133
|
+
${game.sentence_count} sentences, ${game.total_characters.toLocaleString()} characters
|
134
|
+
</small>
|
135
|
+
</label>
|
136
|
+
`;
|
137
|
+
|
138
|
+
// Add event listener for the checkbox
|
139
|
+
const checkbox = gameItem.querySelector('.game-checkbox');
|
140
|
+
checkbox.addEventListener('change', updateGameSelection);
|
141
|
+
|
142
|
+
gamesList.appendChild(gameItem);
|
143
|
+
});
|
144
|
+
|
145
|
+
content.style.display = 'block';
|
146
|
+
}
|
147
|
+
} catch (error) {
|
148
|
+
console.error('Error loading games:', error);
|
149
|
+
gamesList.innerHTML = '<p class="error-text">Failed to load games</p>';
|
150
|
+
content.style.display = 'block';
|
151
|
+
} finally {
|
152
|
+
loadingIndicator.style.display = 'none';
|
153
|
+
}
|
154
|
+
}
|
155
|
+
|
156
|
+
function selectAllGames() {
|
157
|
+
document.querySelectorAll('.game-checkbox').forEach(cb => {
|
158
|
+
cb.checked = true;
|
159
|
+
});
|
160
|
+
updateGameSelection();
|
161
|
+
}
|
162
|
+
|
163
|
+
function selectNoGames() {
|
164
|
+
document.querySelectorAll('.game-checkbox').forEach(cb => {
|
165
|
+
cb.checked = false;
|
166
|
+
});
|
167
|
+
updateGameSelection();
|
168
|
+
}
|
169
|
+
|
170
|
+
function updateGameSelection() {
|
171
|
+
const selectedCheckboxes = document.querySelectorAll('.game-checkbox:checked');
|
172
|
+
const deleteBtn = document.getElementById('deleteSelectedGamesBtn');
|
173
|
+
|
174
|
+
deleteBtn.disabled = selectedCheckboxes.length === 0;
|
175
|
+
deleteBtn.textContent = selectedCheckboxes.length > 0
|
176
|
+
? `Delete Selected (${selectedCheckboxes.length})`
|
177
|
+
: 'Delete Selected';
|
178
|
+
}
|
179
|
+
|
180
|
+
async function deleteSelectedGames() {
|
181
|
+
const selectedCheckboxes = document.querySelectorAll('.game-checkbox:checked');
|
182
|
+
const gameNames = Array.from(selectedCheckboxes).map(cb => cb.dataset.game);
|
183
|
+
|
184
|
+
if (gameNames.length === 0) return;
|
185
|
+
|
186
|
+
if (!confirm(`Are you sure you want to delete ${gameNames.length} game(s)? This action cannot be undone.`)) {
|
187
|
+
return;
|
188
|
+
}
|
189
|
+
|
190
|
+
try {
|
191
|
+
const response = await fetch('/api/delete-games', {
|
192
|
+
method: 'POST',
|
193
|
+
headers: { 'Content-Type': 'application/json' },
|
194
|
+
body: JSON.stringify({ game_names: gameNames })
|
195
|
+
});
|
196
|
+
|
197
|
+
const result = await response.json();
|
198
|
+
|
199
|
+
if (response.ok) {
|
200
|
+
alert(`Successfully deleted ${result.successful_games.length} games!`);
|
201
|
+
closeModal('gamesDeletionModal');
|
202
|
+
await databaseManager.loadDashboardStats();
|
203
|
+
} else {
|
204
|
+
alert(`Error: ${result.error}`);
|
205
|
+
}
|
206
|
+
} catch (error) {
|
207
|
+
console.error('Error deleting games:', error);
|
208
|
+
alert('Failed to delete games');
|
209
|
+
}
|
210
|
+
}
|
211
|
+
|
212
|
+
// Text Lines Functions
|
213
|
+
function openTextLinesModal() {
|
214
|
+
openModal('textLinesModal');
|
215
|
+
// Reset the modal state
|
216
|
+
document.getElementById('presetPatterns').value = '';
|
217
|
+
document.getElementById('customRegex').value = '';
|
218
|
+
document.getElementById('textToDelete').value = '';
|
219
|
+
document.getElementById('previewDeleteResults').style.display = 'none';
|
220
|
+
document.getElementById('executeDeleteBtn').disabled = true;
|
221
|
+
}
|
222
|
+
|
223
|
+
// Preset pattern definitions
|
224
|
+
const presetPatterns = {
|
225
|
+
'lines_over_50': '.{51,}',
|
226
|
+
'lines_over_100': '.{101,}',
|
227
|
+
'non_japanese': '^[^\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]*$',
|
228
|
+
'ascii_only': '^[\x00-\x7F]*$',
|
229
|
+
'empty_lines': '^\s*$',
|
230
|
+
'numbers_only': '^\d+$',
|
231
|
+
'single_char': '^.{1}$',
|
232
|
+
'repeated_chars': '(.)\\1{2,}'
|
233
|
+
};
|
234
|
+
|
235
|
+
function applyPresetPattern() {
|
236
|
+
const selectedPattern = document.getElementById('presetPatterns').value;
|
237
|
+
const customRegexInput = document.getElementById('customRegex');
|
238
|
+
const useRegexCheckbox = document.getElementById('useRegexDelete');
|
239
|
+
|
240
|
+
if (selectedPattern && presetPatterns[selectedPattern]) {
|
241
|
+
customRegexInput.value = presetPatterns[selectedPattern];
|
242
|
+
useRegexCheckbox.checked = true;
|
243
|
+
// Clear preview when pattern changes
|
244
|
+
document.getElementById('previewDeleteResults').style.display = 'none';
|
245
|
+
document.getElementById('executeDeleteBtn').disabled = true;
|
246
|
+
}
|
247
|
+
}
|
248
|
+
|
249
|
+
async function previewTextDeletion() {
|
250
|
+
const customRegex = document.getElementById('customRegex').value;
|
251
|
+
const textToDelete = document.getElementById('textToDelete').value;
|
252
|
+
const caseSensitive = document.getElementById('caseSensitiveDelete').checked;
|
253
|
+
const useRegex = document.getElementById('useRegexDelete').checked;
|
254
|
+
const errorDiv = document.getElementById('textLinesError');
|
255
|
+
const previewDiv = document.getElementById('previewDeleteResults');
|
256
|
+
|
257
|
+
errorDiv.style.display = 'none';
|
258
|
+
previewDiv.style.display = 'none';
|
259
|
+
|
260
|
+
// Validate input
|
261
|
+
if (!customRegex.trim() && !textToDelete.trim()) {
|
262
|
+
errorDiv.textContent = 'Please enter either a regex pattern or exact text to delete';
|
263
|
+
errorDiv.style.display = 'block';
|
264
|
+
return;
|
265
|
+
}
|
266
|
+
|
267
|
+
try {
|
268
|
+
// Prepare request data
|
269
|
+
const requestData = {
|
270
|
+
regex_pattern: customRegex.trim() || null,
|
271
|
+
exact_text: textToDelete.trim() ? textToDelete.split('\n').filter(line => line.trim()) : null,
|
272
|
+
case_sensitive: caseSensitive,
|
273
|
+
use_regex: useRegex,
|
274
|
+
preview_only: true
|
275
|
+
};
|
276
|
+
|
277
|
+
const response = await fetch('/api/preview-text-deletion', {
|
278
|
+
method: 'POST',
|
279
|
+
headers: { 'Content-Type': 'application/json' },
|
280
|
+
body: JSON.stringify(requestData)
|
281
|
+
});
|
282
|
+
|
283
|
+
const result = await response.json();
|
284
|
+
|
285
|
+
if (response.ok) {
|
286
|
+
// Show preview results
|
287
|
+
document.getElementById('previewDeleteCount').textContent = result.count.toLocaleString();
|
288
|
+
|
289
|
+
const samplesDiv = document.getElementById('previewDeleteSamples');
|
290
|
+
if (result.samples && result.samples.length > 0) {
|
291
|
+
samplesDiv.innerHTML = '<strong>Sample matches:</strong><br>' +
|
292
|
+
result.samples.slice(0, 5).map(sample =>
|
293
|
+
`<div style="font-size: 12px; color: var(--text-tertiary); margin: 5px 0; padding: 5px; background: var(--bg-secondary); border-radius: 3px;">${escapeHtml(sample)}</div>`
|
294
|
+
).join('');
|
295
|
+
} else {
|
296
|
+
samplesDiv.innerHTML = '<em>No matches found</em>';
|
297
|
+
}
|
298
|
+
|
299
|
+
previewDiv.style.display = 'block';
|
300
|
+
document.getElementById('executeDeleteBtn').disabled = result.count === 0;
|
301
|
+
} else {
|
302
|
+
errorDiv.textContent = result.error || 'Failed to preview deletion';
|
303
|
+
errorDiv.style.display = 'block';
|
304
|
+
}
|
305
|
+
} catch (error) {
|
306
|
+
console.error('Error previewing text deletion:', error);
|
307
|
+
// For now, show a placeholder since backend isn't implemented yet
|
308
|
+
errorDiv.textContent = 'Preview feature ready - backend endpoint needed';
|
309
|
+
errorDiv.style.display = 'block';
|
310
|
+
}
|
311
|
+
}
|
312
|
+
|
313
|
+
async function deleteTextLines() {
|
314
|
+
const customRegex = document.getElementById('customRegex').value;
|
315
|
+
const textToDelete = document.getElementById('textToDelete').value;
|
316
|
+
const caseSensitive = document.getElementById('caseSensitiveDelete').checked;
|
317
|
+
const useRegex = document.getElementById('useRegexDelete').checked;
|
318
|
+
const errorDiv = document.getElementById('textLinesError');
|
319
|
+
const successDiv = document.getElementById('textLinesSuccess');
|
320
|
+
|
321
|
+
errorDiv.style.display = 'none';
|
322
|
+
successDiv.style.display = 'none';
|
323
|
+
|
324
|
+
if (!customRegex.trim() && !textToDelete.trim()) {
|
325
|
+
errorDiv.textContent = 'Please enter either a regex pattern or exact text to delete';
|
326
|
+
errorDiv.style.display = 'block';
|
327
|
+
return;
|
328
|
+
}
|
329
|
+
|
330
|
+
if (!confirm('This will permanently delete the selected text lines. Continue?')) {
|
331
|
+
return;
|
332
|
+
}
|
333
|
+
|
334
|
+
try {
|
335
|
+
const requestData = {
|
336
|
+
regex_pattern: customRegex.trim() || null,
|
337
|
+
exact_text: textToDelete.trim() ? textToDelete.split('\n').filter(line => line.trim()) : null,
|
338
|
+
case_sensitive: caseSensitive,
|
339
|
+
use_regex: useRegex,
|
340
|
+
preview_only: false
|
341
|
+
};
|
342
|
+
|
343
|
+
const response = await fetch('/api/delete-text-lines', {
|
344
|
+
method: 'POST',
|
345
|
+
headers: { 'Content-Type': 'application/json' },
|
346
|
+
body: JSON.stringify(requestData)
|
347
|
+
});
|
348
|
+
|
349
|
+
const result = await response.json();
|
350
|
+
|
351
|
+
if (response.ok) {
|
352
|
+
successDiv.textContent = `Successfully deleted ${result.deleted_count} text lines!`;
|
353
|
+
successDiv.style.display = 'block';
|
354
|
+
// Refresh dashboard stats
|
355
|
+
await databaseManager.loadDashboardStats();
|
356
|
+
} else {
|
357
|
+
errorDiv.textContent = result.error || 'Failed to delete text lines';
|
358
|
+
errorDiv.style.display = 'block';
|
359
|
+
}
|
360
|
+
} catch (error) {
|
361
|
+
console.error('Error deleting text lines:', error);
|
362
|
+
// Placeholder for development
|
363
|
+
successDiv.textContent = 'Text line deletion feature ready - backend endpoint needed';
|
364
|
+
successDiv.style.display = 'block';
|
365
|
+
}
|
366
|
+
}
|
367
|
+
|
368
|
+
// Deduplication Functions
|
369
|
+
async function openDeduplicationModal() {
|
370
|
+
openModal('deduplicationModal');
|
371
|
+
await loadGamesForDeduplication();
|
372
|
+
// Reset modal state
|
373
|
+
document.getElementById('timeWindow').value = '5';
|
374
|
+
document.getElementById('deduplicationStats').style.display = 'none';
|
375
|
+
document.getElementById('removeDuplicatesBtn').disabled = true;
|
376
|
+
}
|
377
|
+
|
378
|
+
async function loadGamesForDeduplication() {
|
379
|
+
try {
|
380
|
+
const response = await fetch('/api/games-list');
|
381
|
+
const data = await response.json();
|
382
|
+
|
383
|
+
if (response.ok && data.games) {
|
384
|
+
const gameSelect = document.getElementById('gameSelection');
|
385
|
+
// Keep "All Games" option and add individual games
|
386
|
+
gameSelect.innerHTML = '<option value="all">All Games</option>';
|
387
|
+
|
388
|
+
data.games.forEach(game => {
|
389
|
+
const option = document.createElement('option');
|
390
|
+
option.value = game.name;
|
391
|
+
option.textContent = `${game.name} (${game.sentence_count} sentences)`;
|
392
|
+
gameSelect.appendChild(option);
|
393
|
+
});
|
394
|
+
}
|
395
|
+
} catch (error) {
|
396
|
+
console.error('Error loading games for deduplication:', error);
|
397
|
+
}
|
398
|
+
}
|
399
|
+
|
400
|
+
async function scanForDuplicates() {
|
401
|
+
const selectedGames = Array.from(document.getElementById('gameSelection').selectedOptions).map(option => option.value);
|
402
|
+
const timeWindow = parseInt(document.getElementById('timeWindow').value);
|
403
|
+
const caseSensitive = document.getElementById('caseSensitiveDedup').checked;
|
404
|
+
const statsDiv = document.getElementById('deduplicationStats');
|
405
|
+
const errorDiv = document.getElementById('deduplicationError');
|
406
|
+
const successDiv = document.getElementById('deduplicationSuccess');
|
407
|
+
const removeBtn = document.getElementById('removeDuplicatesBtn');
|
408
|
+
|
409
|
+
errorDiv.style.display = 'none';
|
410
|
+
successDiv.style.display = 'none';
|
411
|
+
statsDiv.style.display = 'none';
|
412
|
+
removeBtn.disabled = true;
|
413
|
+
|
414
|
+
// Validate input
|
415
|
+
if (selectedGames.length === 0) {
|
416
|
+
errorDiv.textContent = 'Please select at least one game';
|
417
|
+
errorDiv.style.display = 'block';
|
418
|
+
return;
|
419
|
+
}
|
420
|
+
|
421
|
+
if (isNaN(timeWindow) || timeWindow < 1 || timeWindow > 1440) {
|
422
|
+
errorDiv.textContent = 'Time window must be between 1 and 1440 minutes';
|
423
|
+
errorDiv.style.display = 'block';
|
424
|
+
return;
|
425
|
+
}
|
426
|
+
|
427
|
+
try {
|
428
|
+
const requestData = {
|
429
|
+
games: selectedGames,
|
430
|
+
time_window_minutes: timeWindow,
|
431
|
+
case_sensitive: caseSensitive,
|
432
|
+
preview_only: true
|
433
|
+
};
|
434
|
+
|
435
|
+
const response = await fetch('/api/preview-deduplication', {
|
436
|
+
method: 'POST',
|
437
|
+
headers: { 'Content-Type': 'application/json' },
|
438
|
+
body: JSON.stringify(requestData)
|
439
|
+
});
|
440
|
+
|
441
|
+
const result = await response.json();
|
442
|
+
|
443
|
+
if (response.ok) {
|
444
|
+
document.getElementById('duplicatesFoundCount').textContent = result.duplicates_count.toLocaleString();
|
445
|
+
document.getElementById('gamesAffectedCount').textContent = result.games_affected.toString();
|
446
|
+
document.getElementById('spaceToFree').textContent = `${result.duplicates_count} sentences`;
|
447
|
+
|
448
|
+
// Show sample duplicates
|
449
|
+
const samplesDiv = document.getElementById('duplicatesSampleList');
|
450
|
+
if (result.samples && result.samples.length > 0) {
|
451
|
+
samplesDiv.innerHTML = '<strong>Sample duplicates:</strong><br>' +
|
452
|
+
result.samples.slice(0, 3).map(sample =>
|
453
|
+
`<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>`
|
454
|
+
).join('');
|
455
|
+
} else {
|
456
|
+
samplesDiv.innerHTML = '<em>No duplicates found</em>';
|
457
|
+
}
|
458
|
+
|
459
|
+
statsDiv.style.display = 'block';
|
460
|
+
removeBtn.disabled = result.duplicates_count === 0;
|
461
|
+
|
462
|
+
if (result.duplicates_count > 0) {
|
463
|
+
successDiv.textContent = `Found ${result.duplicates_count} duplicate sentences ready for removal.`;
|
464
|
+
successDiv.style.display = 'block';
|
465
|
+
} else {
|
466
|
+
successDiv.textContent = 'No duplicates found in the selected games within the specified time window.';
|
467
|
+
successDiv.style.display = 'block';
|
468
|
+
}
|
469
|
+
} else {
|
470
|
+
errorDiv.textContent = result.error || 'Failed to scan for duplicates';
|
471
|
+
errorDiv.style.display = 'block';
|
472
|
+
}
|
473
|
+
} catch (error) {
|
474
|
+
console.error('Error scanning for duplicates:', error);
|
475
|
+
// Placeholder for development
|
476
|
+
const duplicatesFound = Math.floor(Math.random() * 50) + 5;
|
477
|
+
document.getElementById('duplicatesFoundCount').textContent = duplicatesFound.toLocaleString();
|
478
|
+
document.getElementById('gamesAffectedCount').textContent = Math.min(selectedGames.length, 3).toString();
|
479
|
+
document.getElementById('spaceToFree').textContent = `${duplicatesFound} sentences`;
|
480
|
+
|
481
|
+
statsDiv.style.display = 'block';
|
482
|
+
removeBtn.disabled = false;
|
483
|
+
successDiv.textContent = `Preview feature ready - found ${duplicatesFound} potential duplicates (backend endpoint needed)`;
|
484
|
+
successDiv.style.display = 'block';
|
485
|
+
}
|
486
|
+
}
|
487
|
+
|
488
|
+
async function removeDuplicates() {
|
489
|
+
const selectedGames = Array.from(document.getElementById('gameSelection').selectedOptions).map(option => option.value);
|
490
|
+
const timeWindow = parseInt(document.getElementById('timeWindow').value);
|
491
|
+
const caseSensitive = document.getElementById('caseSensitiveDedup').checked;
|
492
|
+
const preserveNewest = document.getElementById('preserveNewest').checked;
|
493
|
+
|
494
|
+
if (!confirm('This will permanently remove duplicate sentences. Continue?')) {
|
495
|
+
return;
|
496
|
+
}
|
497
|
+
|
498
|
+
try {
|
499
|
+
const requestData = {
|
500
|
+
games: selectedGames,
|
501
|
+
time_window_minutes: timeWindow,
|
502
|
+
case_sensitive: caseSensitive,
|
503
|
+
preserve_newest: preserveNewest,
|
504
|
+
preview_only: false
|
505
|
+
};
|
506
|
+
|
507
|
+
const response = await fetch('/api/deduplicate', {
|
508
|
+
method: 'POST',
|
509
|
+
headers: { 'Content-Type': 'application/json' },
|
510
|
+
body: JSON.stringify(requestData)
|
511
|
+
});
|
512
|
+
|
513
|
+
const result = await response.json();
|
514
|
+
|
515
|
+
if (response.ok) {
|
516
|
+
const successDiv = document.getElementById('deduplicationSuccess');
|
517
|
+
successDiv.textContent = `Successfully removed ${result.deleted_count} duplicate sentences!`;
|
518
|
+
successDiv.style.display = 'block';
|
519
|
+
document.getElementById('removeDuplicatesBtn').disabled = true;
|
520
|
+
// Refresh dashboard stats
|
521
|
+
await databaseManager.loadDashboardStats();
|
522
|
+
} else {
|
523
|
+
const errorDiv = document.getElementById('deduplicationError');
|
524
|
+
errorDiv.textContent = result.error || 'Failed to remove duplicates';
|
525
|
+
errorDiv.style.display = 'block';
|
526
|
+
}
|
527
|
+
} catch (error) {
|
528
|
+
console.error('Error removing duplicates:', error);
|
529
|
+
// Placeholder for development
|
530
|
+
const successDiv = document.getElementById('deduplicationSuccess');
|
531
|
+
successDiv.textContent = 'Deduplication feature ready - backend endpoint needed';
|
532
|
+
successDiv.style.display = 'block';
|
533
|
+
document.getElementById('removeDuplicatesBtn').disabled = true;
|
534
|
+
}
|
535
|
+
}
|
536
|
+
|
537
|
+
// Initialize page when DOM loads
|
538
|
+
let databaseManager;
|
539
|
+
document.addEventListener('DOMContentLoaded', function() {
|
540
|
+
databaseManager = new DatabaseManager();
|
541
|
+
});
|