|
69 | 69 | const btn = document.getElementById('dict-lookup-btn'); |
70 | 70 | if (!input || !btn) return; |
71 | 71 |
|
| 72 | + // Create refresh button (hidden until first lookup) |
| 73 | + const refreshBtn = document.createElement('button'); |
| 74 | + refreshBtn.id = 'dict-refresh-btn'; |
| 75 | + refreshBtn.className = 'dict-refresh-btn'; |
| 76 | + refreshBtn.title = 'Re-fetch from all sources'; |
| 77 | + refreshBtn.innerHTML = '↻'; |
| 78 | + refreshBtn.style.display = 'none'; |
| 79 | + btn.insertAdjacentElement('afterend', refreshBtn); |
| 80 | + refreshBtn.addEventListener('click', () => { |
| 81 | + const word = input.value.trim(); |
| 82 | + if (word) performLookup(word, true); |
| 83 | + }); |
| 84 | + |
72 | 85 | input.addEventListener('keydown', (e) => { |
73 | 86 | if (e.key === 'Enter') { |
74 | 87 | e.preventDefault(); |
|
84 | 97 | renderAiUsageStats(); |
85 | 98 | } |
86 | 99 |
|
87 | | - async function performLookup(word) { |
| 100 | + async function performLookup(word, refresh = false) { |
88 | 101 | if (!word) return; |
89 | 102 |
|
90 | 103 | const thisLookupId = ++lookupCounter; |
|
124 | 137 |
|
125 | 138 | // 2. Online dictionary + Wiktionary etymology (parallel) |
126 | 139 | let etymologyHtml = null; |
| 140 | + let etymologyCached = false; |
127 | 141 | try { |
128 | 142 | const [onlineResult, etymResult] = await Promise.allSettled([ |
129 | | - invoke('lookup_word_online', { word }), |
130 | | - invoke('lookup_etymology', { word }), |
| 143 | + invoke('lookup_word_online', { word, refresh }), |
| 144 | + invoke('lookup_etymology', { word, refresh }), |
131 | 145 | ]); |
132 | 146 | if (isStale()) { if (btn) btn.disabled = false; return; } |
133 | 147 |
|
134 | | - // Process online dictionary result |
| 148 | + // Process online dictionary result (new shape: { result, cached }) |
135 | 149 | if (onlineResult.status === 'fulfilled' && onlineResult.value) { |
136 | | - const onlineEntry = onlineResult.value; |
137 | | - hasOnlineResults = true; |
138 | | - const onlineHtml = renderDictEntry(onlineEntry, 'online', onlineEntry.source || 'Online'); |
139 | | - if (hasOfflineResults) { |
140 | | - resultsEl.insertAdjacentHTML('beforeend', onlineHtml); |
141 | | - } else { |
142 | | - resultsEl.innerHTML = onlineHtml; |
| 150 | + const onlineResp = onlineResult.value; |
| 151 | + if (onlineResp.result) { |
| 152 | + const onlineEntry = onlineResp.result; |
| 153 | + hasOnlineResults = true; |
| 154 | + const onlineHtml = renderDictEntry(onlineEntry, 'online', onlineEntry.source || 'Online', onlineResp.cached); |
| 155 | + if (hasOfflineResults) { |
| 156 | + resultsEl.insertAdjacentHTML('beforeend', onlineHtml); |
| 157 | + } else { |
| 158 | + resultsEl.innerHTML = onlineHtml; |
| 159 | + } |
| 160 | + wireUpDictTags(resultsEl); |
| 161 | + wireUpPronounceButtons(resultsEl); |
143 | 162 | } |
144 | | - wireUpDictTags(resultsEl); |
145 | | - wireUpPronounceButtons(resultsEl); |
146 | 163 | } |
147 | 164 |
|
148 | | - // Process Wiktionary etymology result |
| 165 | + // Process Wiktionary etymology result (new shape: { result, cached }) |
149 | 166 | if (etymResult.status === 'fulfilled' && etymResult.value) { |
150 | | - etymologyHtml = etymResult.value.etymology_html; |
151 | | - const etymSection = '<div class="dict-etymology dict-etymology-wiktionary">' |
152 | | - + '<div class="dict-etymology-label">Etymology <span class="dict-etymology-source">' |
153 | | - + `(${escapeHtml(etymResult.value.source)})</span></div>` |
154 | | - + `<div class="dict-etymology-text">${sanitizeHtml(etymologyHtml)}</div></div>`; |
155 | | - resultsEl.insertAdjacentHTML('beforeend', etymSection); |
| 167 | + const etymResp = etymResult.value; |
| 168 | + etymologyCached = etymResp.cached; |
| 169 | + if (etymResp.result) { |
| 170 | + etymologyHtml = etymResp.result.etymology_html; |
| 171 | + const cachedTag = etymResp.cached |
| 172 | + ? ' <span class="dict-cached-badge">cached</span>' |
| 173 | + : ''; |
| 174 | + const etymSection = '<div class="dict-etymology dict-etymology-wiktionary">' |
| 175 | + + '<div class="dict-etymology-label">Etymology <span class="dict-etymology-source">' |
| 176 | + + `(${escapeHtml(etymResp.result.source)})</span>${cachedTag}</div>` |
| 177 | + + `<div class="dict-etymology-text">${sanitizeHtml(etymologyHtml)}</div></div>`; |
| 178 | + resultsEl.insertAdjacentHTML('beforeend', etymSection); |
| 179 | + } |
156 | 180 | } |
157 | 181 | } catch (_err) { |
158 | 182 | if (isStale()) { if (btn) btn.disabled = false; return; } |
|
165 | 189 | '<div class="dict-ai-loading">Loading AI definition...</div>'); |
166 | 190 | } |
167 | 191 |
|
168 | | - const response = await invoke('lookup_word', { word }); |
| 192 | + const response = await invoke('lookup_word', { word, refresh }); |
169 | 193 | if (isStale()) { if (btn) btn.disabled = false; return; } |
170 | 194 |
|
171 | 195 | const entry = response; |
|
178 | 202 | const aiLoadingEl = resultsEl.querySelector('.dict-ai-loading'); |
179 | 203 | if (aiLoadingEl) aiLoadingEl.remove(); |
180 | 204 |
|
181 | | - const aiHtml = renderDictEntry(entry, 'ai', 'AI'); |
| 205 | + const aiHtml = renderDictEntry(entry, 'ai', 'AI', response.cached); |
182 | 206 | if (hasAnyResults()) { |
183 | 207 | resultsEl.insertAdjacentHTML('beforeend', aiHtml); |
184 | 208 | } else { |
|
220 | 244 | if (!hasAnyResults() && !hasAiResults) { |
221 | 245 | resultsEl.innerHTML = '<div class="dict-empty-state">No results found</div>'; |
222 | 246 | } |
| 247 | + |
| 248 | + // Show the refresh button in the input bar |
| 249 | + if (!isStale()) { |
| 250 | + const refreshBtn = document.getElementById('dict-refresh-btn'); |
| 251 | + if (refreshBtn) refreshBtn.style.display = ''; |
| 252 | + } |
223 | 253 | } |
224 | 254 |
|
225 | 255 | function renderOfflineResults(results) { |
|
239 | 269 | * @param {Object} entry - DictionaryEntry {word, phonetic, definitions, synonyms, antonyms} |
240 | 270 | * @param {string} badgeClass - CSS class for the source badge (e.g. 'ai', 'online') |
241 | 271 | * @param {string} badgeLabel - Display text for the source badge |
| 272 | + * @param {boolean} [cached=false] - Whether this result was served from cache |
242 | 273 | */ |
243 | | - function renderDictEntry(entry, badgeClass, badgeLabel) { |
| 274 | + function renderDictEntry(entry, badgeClass, badgeLabel, cached) { |
244 | 275 | let html = `<div class="dict-entry">`; |
245 | 276 |
|
246 | 277 | // Word header with source badge, phonetic and pronounce button |
247 | 278 | html += '<div class="dict-word-header">'; |
248 | 279 | html += `<div class="dict-source-badge ${escapeHtml(badgeClass)}">${escapeHtml(badgeLabel)}</div>`; |
| 280 | + if (cached) { |
| 281 | + html += '<span class="dict-cached-badge">cached</span>'; |
| 282 | + } |
249 | 283 | html += `<h1 class="dict-word-title">${escapeHtml(entry.word)}</h1>`; |
250 | 284 | html += `<button class="dict-pronounce-btn" data-word="${escapeHtml(entry.word)}" title="Pronounce">🔊</button>`; |
251 | 285 | if (entry.phonetic) { |
|
0 commit comments