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.
- GameSentenceMiner/__init__.py +39 -0
- GameSentenceMiner/anki.py +6 -3
- GameSentenceMiner/gametext.py +13 -2
- GameSentenceMiner/gsm.py +40 -3
- GameSentenceMiner/locales/en_us.json +4 -0
- GameSentenceMiner/locales/ja_jp.json +4 -0
- GameSentenceMiner/locales/zh_cn.json +4 -0
- GameSentenceMiner/obs.py +4 -1
- GameSentenceMiner/owocr/owocr/ocr.py +304 -134
- GameSentenceMiner/owocr/owocr/run.py +1 -1
- GameSentenceMiner/ui/anki_confirmation.py +4 -2
- GameSentenceMiner/ui/config_gui.py +12 -0
- GameSentenceMiner/util/configuration.py +6 -2
- GameSentenceMiner/util/cron/__init__.py +12 -0
- GameSentenceMiner/util/cron/daily_rollup.py +613 -0
- GameSentenceMiner/util/cron/jiten_update.py +397 -0
- GameSentenceMiner/util/cron/populate_games.py +154 -0
- GameSentenceMiner/util/cron/run_crons.py +148 -0
- GameSentenceMiner/util/cron/setup_populate_games_cron.py +118 -0
- GameSentenceMiner/util/cron_table.py +334 -0
- GameSentenceMiner/util/db.py +236 -49
- GameSentenceMiner/util/ffmpeg.py +23 -4
- GameSentenceMiner/util/games_table.py +340 -93
- GameSentenceMiner/util/jiten_api_client.py +188 -0
- GameSentenceMiner/util/stats_rollup_table.py +216 -0
- GameSentenceMiner/web/anki_api_endpoints.py +438 -220
- GameSentenceMiner/web/database_api.py +955 -1259
- GameSentenceMiner/web/jiten_database_api.py +1015 -0
- GameSentenceMiner/web/rollup_stats.py +672 -0
- GameSentenceMiner/web/static/css/dashboard-shared.css +75 -13
- GameSentenceMiner/web/static/css/overview.css +604 -47
- GameSentenceMiner/web/static/css/search.css +226 -0
- GameSentenceMiner/web/static/css/shared.css +762 -0
- GameSentenceMiner/web/static/css/stats.css +221 -0
- GameSentenceMiner/web/static/js/components/bar-chart.js +339 -0
- GameSentenceMiner/web/static/js/database-bulk-operations.js +320 -0
- GameSentenceMiner/web/static/js/database-game-data.js +390 -0
- GameSentenceMiner/web/static/js/database-game-operations.js +213 -0
- GameSentenceMiner/web/static/js/database-helpers.js +44 -0
- GameSentenceMiner/web/static/js/database-jiten-integration.js +750 -0
- GameSentenceMiner/web/static/js/database-popups.js +89 -0
- GameSentenceMiner/web/static/js/database-tabs.js +64 -0
- GameSentenceMiner/web/static/js/database-text-management.js +371 -0
- GameSentenceMiner/web/static/js/database.js +86 -718
- GameSentenceMiner/web/static/js/goals.js +79 -18
- GameSentenceMiner/web/static/js/heatmap.js +29 -23
- GameSentenceMiner/web/static/js/overview.js +1205 -339
- GameSentenceMiner/web/static/js/regex-patterns.js +100 -0
- GameSentenceMiner/web/static/js/search.js +215 -18
- GameSentenceMiner/web/static/js/shared.js +193 -39
- GameSentenceMiner/web/static/js/stats.js +1536 -179
- GameSentenceMiner/web/stats.py +1142 -269
- GameSentenceMiner/web/stats_api.py +2104 -0
- GameSentenceMiner/web/templates/anki_stats.html +4 -18
- GameSentenceMiner/web/templates/components/date-range.html +118 -3
- GameSentenceMiner/web/templates/components/html-head.html +40 -6
- GameSentenceMiner/web/templates/components/js-config.html +8 -8
- GameSentenceMiner/web/templates/components/regex-input.html +160 -0
- GameSentenceMiner/web/templates/database.html +564 -117
- GameSentenceMiner/web/templates/goals.html +41 -5
- GameSentenceMiner/web/templates/overview.html +159 -129
- GameSentenceMiner/web/templates/search.html +78 -9
- GameSentenceMiner/web/templates/stats.html +159 -5
- GameSentenceMiner/web/texthooking_page.py +280 -111
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/METADATA +43 -2
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/RECORD +70 -47
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
+
}
|