GameSentenceMiner 2.15.10__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/anki.py +31 -0
- GameSentenceMiner/ocr/owocr_helper.py +5 -5
- 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/anki_stats.html +205 -0
- GameSentenceMiner/web/templates/components/navigation.html +16 -0
- GameSentenceMiner/web/templates/components/theme-styles.html +128 -0
- GameSentenceMiner/web/templates/stats.html +4 -0
- GameSentenceMiner/web/texthooking_page.py +50 -0
- {gamesentenceminer-2.15.10.dist-info → gamesentenceminer-2.15.12.dist-info}/METADATA +1 -1
- {gamesentenceminer-2.15.10.dist-info → gamesentenceminer-2.15.12.dist-info}/RECORD +23 -11
- GameSentenceMiner/web/templates/text_replacements.html +0 -449
- {gamesentenceminer-2.15.10.dist-info → gamesentenceminer-2.15.12.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.15.10.dist-info → gamesentenceminer-2.15.12.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.15.10.dist-info → gamesentenceminer-2.15.12.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.15.10.dist-info → gamesentenceminer-2.15.12.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,203 @@
|
|
1
|
+
/**
|
2
|
+
* Shared Kanji Grid Renderer Component
|
3
|
+
* Handles rendering kanji grids with configurable options for different use cases
|
4
|
+
*/
|
5
|
+
|
6
|
+
class KanjiGridRenderer {
|
7
|
+
constructor(options = {}) {
|
8
|
+
// Default configuration
|
9
|
+
this.config = {
|
10
|
+
containerSelector: '#kanjiGrid',
|
11
|
+
counterSelector: '#kanjiCount',
|
12
|
+
colorMode: 'backend', // 'backend' or 'frequency'
|
13
|
+
clickHandler: null, // Custom click handler, defaults to search navigation
|
14
|
+
emptyMessage: 'No kanji data available',
|
15
|
+
showCounter: true,
|
16
|
+
showLegend: true,
|
17
|
+
...options
|
18
|
+
};
|
19
|
+
|
20
|
+
// Get DOM elements
|
21
|
+
this.container = document.querySelector(this.config.containerSelector);
|
22
|
+
this.counter = this.config.counterSelector ? document.querySelector(this.config.counterSelector) : null;
|
23
|
+
|
24
|
+
if (!this.container) {
|
25
|
+
console.error(`KanjiGridRenderer: Container not found with selector: ${this.config.containerSelector}`);
|
26
|
+
return;
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
/**
|
31
|
+
* Render the kanji grid with provided data
|
32
|
+
* @param {Object|Array} kanjiData - Kanji data to render
|
33
|
+
*/
|
34
|
+
render(kanjiData) {
|
35
|
+
if (!kanjiData) {
|
36
|
+
this.renderEmpty();
|
37
|
+
return;
|
38
|
+
}
|
39
|
+
|
40
|
+
// Handle different data formats
|
41
|
+
let kanjiList = [];
|
42
|
+
if (Array.isArray(kanjiData)) {
|
43
|
+
kanjiList = kanjiData;
|
44
|
+
} else if (kanjiData.kanji_data && Array.isArray(kanjiData.kanji_data)) {
|
45
|
+
kanjiList = kanjiData.kanji_data;
|
46
|
+
// Update counter if available
|
47
|
+
if (this.counter && this.config.showCounter) {
|
48
|
+
this.counter.textContent = kanjiData.unique_count || kanjiList.length;
|
49
|
+
}
|
50
|
+
} else {
|
51
|
+
this.renderEmpty();
|
52
|
+
return;
|
53
|
+
}
|
54
|
+
|
55
|
+
if (kanjiList.length === 0) {
|
56
|
+
this.renderEmpty();
|
57
|
+
return;
|
58
|
+
}
|
59
|
+
|
60
|
+
// Update counter
|
61
|
+
if (this.counter && this.config.showCounter) {
|
62
|
+
this.counter.textContent = kanjiList.length;
|
63
|
+
}
|
64
|
+
|
65
|
+
// Clear existing grid
|
66
|
+
this.container.innerHTML = '';
|
67
|
+
|
68
|
+
// Render kanji cells
|
69
|
+
kanjiList.forEach(item => {
|
70
|
+
const cell = this.createKanjiCell(item);
|
71
|
+
this.container.appendChild(cell);
|
72
|
+
});
|
73
|
+
}
|
74
|
+
|
75
|
+
/**
|
76
|
+
* Create individual kanji cell element
|
77
|
+
* @param {Object} item - Kanji item with kanji character and frequency
|
78
|
+
* @returns {HTMLElement} - Kanji cell element
|
79
|
+
*/
|
80
|
+
createKanjiCell(item) {
|
81
|
+
const cell = document.createElement('span');
|
82
|
+
cell.className = 'kanji-cell';
|
83
|
+
cell.textContent = item.kanji;
|
84
|
+
|
85
|
+
// Apply colors based on mode
|
86
|
+
if (this.config.colorMode === 'backend' && item.color) {
|
87
|
+
// Use backend-provided color
|
88
|
+
cell.style.backgroundColor = item.color;
|
89
|
+
// Determine text color based on background brightness
|
90
|
+
const brightness = this.getColorBrightness(item.color);
|
91
|
+
cell.style.color = brightness > 128 ? '#333' : '#fff';
|
92
|
+
} else if (this.config.colorMode === 'frequency' && item.frequency) {
|
93
|
+
// Calculate color based on frequency
|
94
|
+
const color = this.getFrequencyColor(item.frequency);
|
95
|
+
cell.style.backgroundColor = color;
|
96
|
+
// Determine text color for better contrast
|
97
|
+
if (item.frequency > 100) {
|
98
|
+
cell.style.color = 'white';
|
99
|
+
}
|
100
|
+
}
|
101
|
+
|
102
|
+
// Add tooltip
|
103
|
+
cell.title = `${item.kanji}: ${item.frequency} encounters`;
|
104
|
+
|
105
|
+
// Add click handler
|
106
|
+
const clickHandler = this.config.clickHandler || this.defaultClickHandler;
|
107
|
+
if (clickHandler) {
|
108
|
+
cell.style.cursor = 'pointer';
|
109
|
+
cell.addEventListener('click', () => clickHandler(item));
|
110
|
+
}
|
111
|
+
|
112
|
+
return cell;
|
113
|
+
}
|
114
|
+
|
115
|
+
/**
|
116
|
+
* Default click handler - navigate to search page
|
117
|
+
* @param {Object} item - Kanji item
|
118
|
+
*/
|
119
|
+
defaultClickHandler(item) {
|
120
|
+
window.location.href = `/search?q=${encodeURIComponent(item.kanji)}`;
|
121
|
+
}
|
122
|
+
|
123
|
+
/**
|
124
|
+
* Get color based on frequency (client-side calculation)
|
125
|
+
* @param {number} frequency - Kanji frequency
|
126
|
+
* @returns {string} - CSS color value
|
127
|
+
*/
|
128
|
+
getFrequencyColor(frequency) {
|
129
|
+
if (frequency > 500) return '#2ee6e0'; // Cyan for very frequent
|
130
|
+
else if (frequency > 100) return '#3be62f'; // Green for frequent
|
131
|
+
else if (frequency > 30) return '#e6dc2e'; // Yellow for moderate
|
132
|
+
else if (frequency > 10) return '#e6342e'; // Red for occasional
|
133
|
+
else return '#ebedf0'; // Gray for rare
|
134
|
+
}
|
135
|
+
|
136
|
+
/**
|
137
|
+
* Calculate color brightness for text contrast
|
138
|
+
* @param {string} hexColor - Hex color value
|
139
|
+
* @returns {number} - Brightness value (0-255)
|
140
|
+
*/
|
141
|
+
getColorBrightness(hexColor) {
|
142
|
+
// Convert hex to RGB
|
143
|
+
const hex = hexColor.replace('#', '');
|
144
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
145
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
146
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
147
|
+
|
148
|
+
// Calculate brightness using standard formula
|
149
|
+
return (r * 299 + g * 587 + b * 114) / 1000;
|
150
|
+
}
|
151
|
+
|
152
|
+
/**
|
153
|
+
* Render empty state
|
154
|
+
*/
|
155
|
+
renderEmpty() {
|
156
|
+
this.container.innerHTML = `<div style="color:var(--text-secondary);padding:16px;text-align:center;">${this.config.emptyMessage}</div>`;
|
157
|
+
|
158
|
+
if (this.counter && this.config.showCounter) {
|
159
|
+
this.counter.textContent = '0';
|
160
|
+
}
|
161
|
+
}
|
162
|
+
|
163
|
+
/**
|
164
|
+
* Create and render legend (static, always the same)
|
165
|
+
* @param {HTMLElement} legendContainer - Container for legend
|
166
|
+
*/
|
167
|
+
renderLegend(legendContainer) {
|
168
|
+
if (!legendContainer || !this.config.showLegend) return;
|
169
|
+
|
170
|
+
legendContainer.innerHTML = `
|
171
|
+
<div class="kanji-legend">
|
172
|
+
<span>Rarely Seen</span>
|
173
|
+
<div class="kanji-legend-item" style="background-color: #ebedf0;" title="No encounters"></div>
|
174
|
+
<div class="kanji-legend-item" style="background-color: #e6342e;" title="Seen once"></div>
|
175
|
+
<div class="kanji-legend-item" style="background-color: #e6dc2e;" title="Occasionally seen"></div>
|
176
|
+
<div class="kanji-legend-item" style="background-color: #3be62f;" title="Frequently seen"></div>
|
177
|
+
<div class="kanji-legend-item" style="background-color: #2ee6e0;" title="Most frequently seen"></div>
|
178
|
+
<span>Frequently Seen</span>
|
179
|
+
</div>
|
180
|
+
`;
|
181
|
+
}
|
182
|
+
|
183
|
+
/**
|
184
|
+
* Update configuration
|
185
|
+
* @param {Object} newConfig - New configuration options
|
186
|
+
*/
|
187
|
+
updateConfig(newConfig) {
|
188
|
+
this.config = { ...this.config, ...newConfig };
|
189
|
+
}
|
190
|
+
|
191
|
+
/**
|
192
|
+
* Clear the grid
|
193
|
+
*/
|
194
|
+
clear() {
|
195
|
+
this.container.innerHTML = '';
|
196
|
+
if (this.counter && this.config.showCounter) {
|
197
|
+
this.counter.textContent = '0';
|
198
|
+
}
|
199
|
+
}
|
200
|
+
}
|
201
|
+
|
202
|
+
// Export for use in other scripts
|
203
|
+
window.KanjiGridRenderer = KanjiGridRenderer;
|
@@ -0,0 +1,273 @@
|
|
1
|
+
// Search Page JavaScript
|
2
|
+
// Dependencies: shared.js (provides utility functions like escapeHtml, escapeRegex)
|
3
|
+
|
4
|
+
class SentenceSearchApp {
|
5
|
+
constructor() {
|
6
|
+
this.searchInput = document.getElementById('searchInput');
|
7
|
+
this.gameFilter = document.getElementById('gameFilter');
|
8
|
+
this.sortFilter = document.getElementById('sortFilter');
|
9
|
+
this.searchResults = document.getElementById('searchResults');
|
10
|
+
this.loadingIndicator = document.getElementById('loadingIndicator');
|
11
|
+
this.noResults = document.getElementById('noResults');
|
12
|
+
this.emptyState = document.getElementById('emptyState');
|
13
|
+
this.errorMessage = document.getElementById('errorMessage');
|
14
|
+
this.searchStats = document.getElementById('searchStats');
|
15
|
+
this.searchTime = document.getElementById('searchTime');
|
16
|
+
|
17
|
+
this.currentPage = 1;
|
18
|
+
this.pageSize = 20;
|
19
|
+
this.searchTimeout = null;
|
20
|
+
this.currentQuery = '';
|
21
|
+
this.totalResults = 0;
|
22
|
+
|
23
|
+
// Move initialization logic to async method
|
24
|
+
this.initialize();
|
25
|
+
}
|
26
|
+
|
27
|
+
async initialize() {
|
28
|
+
// Check for ?q= parameter and pre-fill input
|
29
|
+
const urlParams = new URLSearchParams(window.location.search);
|
30
|
+
const qParam = urlParams.get('q');
|
31
|
+
if (qParam) {
|
32
|
+
this.searchInput.value = qParam;
|
33
|
+
}
|
34
|
+
|
35
|
+
this.initializeEventListeners();
|
36
|
+
await this.loadGamesList();
|
37
|
+
|
38
|
+
// Trigger search after games list loads if q param is present
|
39
|
+
if (qParam) {
|
40
|
+
this.performSearch();
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
initializeEventListeners() {
|
45
|
+
// Debounced search input
|
46
|
+
this.searchInput.addEventListener('input', (e) => {
|
47
|
+
clearTimeout(this.searchTimeout);
|
48
|
+
this.searchTimeout = setTimeout(() => {
|
49
|
+
this.performSearch();
|
50
|
+
}, 300);
|
51
|
+
});
|
52
|
+
|
53
|
+
// Filter changes
|
54
|
+
this.gameFilter.addEventListener('change', () => this.performSearch());
|
55
|
+
this.sortFilter.addEventListener('change', () => this.performSearch());
|
56
|
+
|
57
|
+
// Pagination
|
58
|
+
document.getElementById('prevPage').addEventListener('click', () => {
|
59
|
+
if (this.currentPage > 1) {
|
60
|
+
this.currentPage--;
|
61
|
+
this.performSearch();
|
62
|
+
}
|
63
|
+
});
|
64
|
+
|
65
|
+
document.getElementById('nextPage').addEventListener('click', () => {
|
66
|
+
this.currentPage++;
|
67
|
+
this.performSearch();
|
68
|
+
});
|
69
|
+
}
|
70
|
+
|
71
|
+
async loadGamesList() {
|
72
|
+
try {
|
73
|
+
const response = await fetch('/api/games-list');
|
74
|
+
const data = await response.json();
|
75
|
+
|
76
|
+
if (response.ok && data.games) {
|
77
|
+
const gameSelect = this.gameFilter;
|
78
|
+
// Clear existing options except "All Games"
|
79
|
+
gameSelect.innerHTML = '<option value="">All Games</option>';
|
80
|
+
|
81
|
+
data.games.forEach(game => {
|
82
|
+
const option = document.createElement('option');
|
83
|
+
option.value = game.name;
|
84
|
+
option.textContent = game.name;
|
85
|
+
gameSelect.appendChild(option);
|
86
|
+
});
|
87
|
+
}
|
88
|
+
} catch (error) {
|
89
|
+
console.error('Failed to load games list:', error);
|
90
|
+
}
|
91
|
+
}
|
92
|
+
|
93
|
+
async performSearch() {
|
94
|
+
const query = this.searchInput.value.trim();
|
95
|
+
const gameFilter = this.gameFilter.value;
|
96
|
+
const sortBy = this.sortFilter.value;
|
97
|
+
|
98
|
+
// Reset to first page for new searches
|
99
|
+
if (query !== this.currentQuery) {
|
100
|
+
this.currentPage = 1;
|
101
|
+
}
|
102
|
+
this.currentQuery = query;
|
103
|
+
|
104
|
+
// Show appropriate state
|
105
|
+
if (!query) {
|
106
|
+
this.showEmptyState();
|
107
|
+
return;
|
108
|
+
}
|
109
|
+
|
110
|
+
this.showLoadingState();
|
111
|
+
const startTime = Date.now();
|
112
|
+
|
113
|
+
try {
|
114
|
+
const params = new URLSearchParams({
|
115
|
+
q: query,
|
116
|
+
page: this.currentPage,
|
117
|
+
page_size: this.pageSize,
|
118
|
+
sort: sortBy
|
119
|
+
});
|
120
|
+
|
121
|
+
if (gameFilter) {
|
122
|
+
params.append('game', gameFilter);
|
123
|
+
}
|
124
|
+
|
125
|
+
const response = await fetch(`/api/search-sentences?${params}`);
|
126
|
+
const data = await response.json();
|
127
|
+
|
128
|
+
const searchTime = Date.now() - startTime;
|
129
|
+
|
130
|
+
if (!response.ok) {
|
131
|
+
throw new Error(data.error || 'Search failed');
|
132
|
+
}
|
133
|
+
|
134
|
+
this.displayResults(data, searchTime);
|
135
|
+
|
136
|
+
} catch (error) {
|
137
|
+
this.showErrorState(error.message);
|
138
|
+
}
|
139
|
+
}
|
140
|
+
|
141
|
+
displayResults(data, searchTime) {
|
142
|
+
this.hideAllStates();
|
143
|
+
this.totalResults = data.total;
|
144
|
+
|
145
|
+
// Update stats
|
146
|
+
const resultText = data.total === 1 ? 'result' : 'results';
|
147
|
+
this.searchStats.textContent = `${data.total.toLocaleString()} ${resultText} found`;
|
148
|
+
this.searchTime.textContent = `Search completed in ${searchTime}ms`;
|
149
|
+
|
150
|
+
if (data.results.length === 0) {
|
151
|
+
this.showNoResultsState();
|
152
|
+
return;
|
153
|
+
}
|
154
|
+
|
155
|
+
// Render results
|
156
|
+
this.searchResults.innerHTML = '';
|
157
|
+
data.results.forEach(result => {
|
158
|
+
const resultElement = this.createResultElement(result);
|
159
|
+
this.searchResults.appendChild(resultElement);
|
160
|
+
});
|
161
|
+
|
162
|
+
this.updatePagination(data);
|
163
|
+
this.searchResults.style.display = 'block';
|
164
|
+
}
|
165
|
+
|
166
|
+
createResultElement(result) {
|
167
|
+
const div = document.createElement('div');
|
168
|
+
div.className = 'search-result';
|
169
|
+
|
170
|
+
// Highlight search terms
|
171
|
+
const highlightedText = this.highlightSearchTerms(result.sentence, this.currentQuery);
|
172
|
+
|
173
|
+
// Format timestamp to ISO format
|
174
|
+
const date = new Date(result.timestamp * 1000);
|
175
|
+
const formattedDate = date.toISOString().split('T')[0] + ' ' + date.toTimeString().split(' ')[0];
|
176
|
+
|
177
|
+
div.innerHTML = `
|
178
|
+
<div class="result-sentence">${highlightedText}</div>
|
179
|
+
<div class="result-metadata">
|
180
|
+
<div class="metadata-item">
|
181
|
+
<span class="game-tag">${escapeHtml(result.game_name)}</span>
|
182
|
+
</div>
|
183
|
+
<div class="metadata-item">
|
184
|
+
<span class="metadata-label">📅</span>
|
185
|
+
<span class="metadata-value">${formattedDate}</span>
|
186
|
+
</div>
|
187
|
+
${result.translation ? `
|
188
|
+
<div class="metadata-item">
|
189
|
+
<span class="metadata-label">💬</span>
|
190
|
+
<span class="metadata-value">Translation available</span>
|
191
|
+
</div>
|
192
|
+
` : ''}
|
193
|
+
</div>
|
194
|
+
`;
|
195
|
+
|
196
|
+
return div;
|
197
|
+
}
|
198
|
+
|
199
|
+
highlightSearchTerms(text, query) {
|
200
|
+
if (!query) return escapeHtml(text);
|
201
|
+
|
202
|
+
const escapedText = escapeHtml(text);
|
203
|
+
const searchTerms = query.split(' ').filter(term => term.length > 0);
|
204
|
+
|
205
|
+
let result = escapedText;
|
206
|
+
searchTerms.forEach(term => {
|
207
|
+
const regex = new RegExp(`(${escapeRegex(term)})`, 'gi');
|
208
|
+
result = result.replace(regex, '<span class="search-highlight">$1</span>');
|
209
|
+
});
|
210
|
+
|
211
|
+
return result;
|
212
|
+
}
|
213
|
+
|
214
|
+
updatePagination(data) {
|
215
|
+
const pagination = document.getElementById('pagination');
|
216
|
+
const prevBtn = document.getElementById('prevPage');
|
217
|
+
const nextBtn = document.getElementById('nextPage');
|
218
|
+
const pageInfo = document.getElementById('pageInfo');
|
219
|
+
|
220
|
+
const totalPages = Math.ceil(data.total / this.pageSize);
|
221
|
+
|
222
|
+
if (totalPages <= 1) {
|
223
|
+
pagination.style.display = 'none';
|
224
|
+
return;
|
225
|
+
}
|
226
|
+
|
227
|
+
pagination.style.display = 'flex';
|
228
|
+
prevBtn.disabled = this.currentPage <= 1;
|
229
|
+
nextBtn.disabled = this.currentPage >= totalPages;
|
230
|
+
|
231
|
+
const startResult = (this.currentPage - 1) * this.pageSize + 1;
|
232
|
+
const endResult = Math.min(this.currentPage * this.pageSize, data.total);
|
233
|
+
|
234
|
+
pageInfo.textContent = `Page ${this.currentPage} of ${totalPages} (${startResult}-${endResult} of ${data.total})`;
|
235
|
+
}
|
236
|
+
|
237
|
+
showLoadingState() {
|
238
|
+
this.hideAllStates();
|
239
|
+
this.loadingIndicator.style.display = 'flex';
|
240
|
+
}
|
241
|
+
|
242
|
+
showEmptyState() {
|
243
|
+
this.hideAllStates();
|
244
|
+
this.emptyState.style.display = 'block';
|
245
|
+
this.searchStats.textContent = 'Ready to search';
|
246
|
+
this.searchTime.textContent = '';
|
247
|
+
}
|
248
|
+
|
249
|
+
showNoResultsState() {
|
250
|
+
this.hideAllStates();
|
251
|
+
this.noResults.style.display = 'block';
|
252
|
+
}
|
253
|
+
|
254
|
+
showErrorState(message) {
|
255
|
+
this.hideAllStates();
|
256
|
+
this.errorMessage.style.display = 'block';
|
257
|
+
document.getElementById('errorText').textContent = message;
|
258
|
+
}
|
259
|
+
|
260
|
+
hideAllStates() {
|
261
|
+
this.loadingIndicator.style.display = 'none';
|
262
|
+
this.emptyState.style.display = 'none';
|
263
|
+
this.noResults.style.display = 'none';
|
264
|
+
this.errorMessage.style.display = 'none';
|
265
|
+
this.searchResults.style.display = 'none';
|
266
|
+
document.getElementById('pagination').style.display = 'none';
|
267
|
+
}
|
268
|
+
}
|
269
|
+
|
270
|
+
// Initialize the app when DOM is loaded
|
271
|
+
document.addEventListener('DOMContentLoaded', () => {
|
272
|
+
new SentenceSearchApp();
|
273
|
+
});
|