Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/api/providers/anthropic.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ export async function fetchCardData(word, context, config) {
headers: anthropicHeaders(config.apiKey),
body: JSON.stringify({
model,
max_tokens: 1024,
system: buildSystemPrompt(config),
max_tokens: 1024,
temperature: config.temperature ?? 0.3,
system: buildSystemPrompt(config),
messages: [
{ role: 'user', content: `${target} word: "${word}"\nContext: "${context}"` },
],
Expand Down
2 changes: 1 addition & 1 deletion src/api/providers/google.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export async function fetchCardData(word, context, config) {
],
systemInstruction: { parts: [{ text: buildSystemPrompt(config) }] },
generationConfig: {
temperature: 0.3,
temperature: config.temperature ?? 0.3,
maxOutputTokens: 1024,
},
};
Expand Down
2 changes: 1 addition & 1 deletion src/api/providers/lmStudio.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export async function fetchCardData(word, context, config) {
{ role: 'system', content: buildSystemPrompt(config) },
{ role: 'user', content: `${target} word: "${word}"\nContext: "${context}"` },
],
temperature: 0.3,
temperature: config.temperature ?? 0.3,
max_tokens: 1024,
}),
});
Expand Down
2 changes: 1 addition & 1 deletion src/api/providers/openai.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export async function fetchCardData(word, context, config) {
{ role: 'system', content: buildSystemPrompt(config) },
{ role: 'user', content: `${target} word: "${word}"\nContext: "${context}"` },
],
temperature: 0.3,
temperature: config.temperature ?? 0.3,
max_tokens: 1024,
}),
});
Expand Down
38 changes: 36 additions & 2 deletions src/background/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import { fetchCardData } from '../api/llmClient.js';
import { fetchPronunciation } from '../api/pronunciation.js';
import { saveCard } from '../storage/cardStorage.js';
import { saveCard, getAllCards } from '../storage/cardStorage.js';
import { createCard, validateLlmData } from '../shared/cardSchema.js';
import { getConfig } from '../config/configStorage.js';
import { t } from '../i18n/strings.js';
Expand Down Expand Up @@ -41,6 +41,7 @@ chrome.runtime.onStartup.addListener(registerContextMenu);
// Re-register context menu when settings are saved
chrome.runtime.onMessage.addListener((msg) => {
if (msg.type === 'SETTINGS_CHANGED') registerContextMenu();
if (msg.type === 'REVALIDATE_CARD') revalidateCard(msg.cardId, msg.word, msg.context);
});

// ── Context menu handler ───────────────────────────────────────────────────────
Expand Down Expand Up @@ -68,7 +69,7 @@ chrome.contextMenus.onClicked.addListener(async (info, tab) => {
const wiktIpa = await fetchPronunciation(word, config.targetLang).catch(() => null);
if (wiktIpa) data.pronunciation = wiktIpa;

const card = createCard(word, context, tab.url ?? '', data);
const card = createCard(word, context, tab.url ?? '', data, wiktIpa != null);

await saveCard(card);
await chrome.storage.session.remove('lastError');
Expand Down Expand Up @@ -123,3 +124,36 @@ function notify(title, message) {
message,
});
}

// ── Card revalidation ─────────────────────────────────────────────────────────

async function revalidateCard(cardId, word, context) {
const config = await getConfig().catch(() => null);
try {
// Fail fast – no LLM call if the card no longer exists
const cards = await getAllCards();
const existing = cards.find(c => c.id === cardId);
if (!existing) throw new Error('Card not found.');

const raw = await fetchCardData(word, context, { ...config, temperature: 0.1 });
const data = validateLlmData(raw);

const updated = {
...existing,
translation: data.translation,
// Keep Wiktionary IPA; only overwrite if it originally came from the LLM
pronunciation: existing.pronunciationFromLookup ? existing.pronunciation : data.pronunciation,
wordClass: data.wordClass,
grammar: data.grammar,
exampleDA: data.exampleDA,
exampleDE: data.exampleDE,
memoryTip: data.memoryTip,
};

await saveCard(updated);
chrome.runtime.sendMessage({ type: 'CARD_REVALIDATED', card: updated }).catch(() => {});
} catch (err) {
const errorMsg = err.message ?? 'Unknown error.';
chrome.runtime.sendMessage({ type: 'CARD_REVALIDATE_ERROR', cardId, error: errorMsg }).catch(() => {});
}
}
22 changes: 22 additions & 0 deletions src/i18n/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const STRINGS = {
rightClick: 'Rechtsklick auf ein Wort →',
addTo: 'Zu Ordforråd hinzufügen',
delete: 'Löschen',
revalidate: 'Karte mit KI neu prüfen',
fetchTip: 'Modelle neu laden',
noModel: 'LM Studio: Kein Modell geladen.',
unreachable: 'LM Studio nicht erreichbar. Läuft der Server?',
Expand Down Expand Up @@ -89,6 +90,7 @@ const STRINGS = {
rightClick: 'Right-click a word →',
addTo: 'Add to Ordforråd',
delete: 'Delete',
revalidate: 'Re-validate with AI',
fetchTip: 'Reload models',
noModel: 'LM Studio: No model loaded.',
unreachable: 'LM Studio unreachable. Is the server running?',
Expand Down Expand Up @@ -128,6 +130,7 @@ const STRINGS = {
rightClick: 'Højreklik på et ord →',
addTo: 'Føj til Ordforråd',
delete: 'Slet',
revalidate: 'Valider kort igen med AI',
fetchTip: 'Genindlæs modeller',
noModel: 'LM Studio: Ingen model indlæst.',
unreachable: 'LM Studio ikke tilgængelig. Kører serveren?',
Expand Down Expand Up @@ -167,6 +170,7 @@ const STRINGS = {
rightClick: 'Clic droit sur un mot →',
addTo: 'Ajouter à Ordforråd',
delete: 'Supprimer',
revalidate: "Revalider avec l'IA",
fetchTip: 'Recharger les modèles',
noModel: 'LM Studio : aucun modèle chargé.',
unreachable: 'LM Studio inaccessible. Le serveur tourne-t-il ?',
Expand Down Expand Up @@ -206,6 +210,7 @@ const STRINGS = {
rightClick: 'Clic derecho en una palabra →',
addTo: 'Añadir a Ordforråd',
delete: 'Eliminar',
revalidate: 'Revalidar con IA',
fetchTip: 'Recargar modelos',
noModel: 'LM Studio: ningún modelo cargado.',
unreachable: 'LM Studio no disponible. ¿Está corriendo el servidor?',
Expand Down Expand Up @@ -245,6 +250,7 @@ const STRINGS = {
rightClick: 'Clic destro su una parola →',
addTo: 'Aggiungi a Ordforråd',
delete: 'Elimina',
revalidate: 'Riconvalida con IA',
fetchTip: 'Ricarica modelli',
noModel: 'LM Studio: nessun modello caricato.',
unreachable: 'LM Studio non raggiungibile. Il server è in esecuzione?',
Expand Down Expand Up @@ -284,6 +290,7 @@ const STRINGS = {
rightClick: 'Clique direito em uma palavra →',
addTo: 'Adicionar ao Ordforråd',
delete: 'Excluir',
revalidate: 'Revalidar com IA',
fetchTip: 'Recarregar modelos',
noModel: 'LM Studio: nenhum modelo carregado.',
unreachable: 'LM Studio inacessível. O servidor está rodando?',
Expand Down Expand Up @@ -323,6 +330,7 @@ const STRINGS = {
rightClick: 'Rechtsklik op een woord →',
addTo: 'Toevoegen aan Ordforråd',
delete: 'Verwijderen',
revalidate: 'Opnieuw valideren met AI',
fetchTip: 'Modellen herladen',
noModel: 'LM Studio: geen model geladen.',
unreachable: 'LM Studio niet bereikbaar. Draait de server?',
Expand Down Expand Up @@ -362,6 +370,7 @@ const STRINGS = {
rightClick: 'Högerklicka på ett ord →',
addTo: 'Lägg till i Ordforråd',
delete: 'Ta bort',
revalidate: 'Omvalidera med AI',
fetchTip: 'Ladda om modeller',
noModel: 'LM Studio: ingen modell laddad.',
unreachable: 'LM Studio inte nåbar. Kör servern?',
Expand Down Expand Up @@ -401,6 +410,7 @@ const STRINGS = {
rightClick: 'Høyreklikk på et ord →',
addTo: 'Legg til i Ordforråd',
delete: 'Slett',
revalidate: 'Revalider med AI',
fetchTip: 'Last inn modeller på nytt',
noModel: 'LM Studio: ingen modell lastet.',
unreachable: 'LM Studio ikke tilgjengelig. Kjører serveren?',
Expand Down Expand Up @@ -440,6 +450,7 @@ const STRINGS = {
rightClick: 'Napsauta hiiren kakkospainikkeella →',
addTo: 'Lisää Ordforråd-kokoelmaan',
delete: 'Poista',
revalidate: 'Tarkista uudelleen tekoälyllä',
fetchTip: 'Lataa mallit uudelleen',
noModel: 'LM Studio: ei mallia ladattu.',
unreachable: 'LM Studio ei tavoitettavissa. Onko palvelin käynnissä?',
Expand Down Expand Up @@ -479,6 +490,7 @@ const STRINGS = {
rightClick: 'Kliknij prawym przyciskiem słowo →',
addTo: 'Dodaj do Ordforråd',
delete: 'Usuń',
revalidate: 'Ponowna walidacja przez AI',
fetchTip: 'Odśwież modele',
noModel: 'LM Studio: brak załadowanego modelu.',
unreachable: 'LM Studio niedostępne. Czy serwer działa?',
Expand Down Expand Up @@ -518,6 +530,7 @@ const STRINGS = {
rightClick: 'Pravý klik na slovo →',
addTo: 'Přidat do Ordforråd',
delete: 'Smazat',
revalidate: 'Znovu ověřit pomocí AI',
fetchTip: 'Znovu načíst modely',
noModel: 'LM Studio: žádný model není načten.',
unreachable: 'LM Studio není dostupné. Běží server?',
Expand Down Expand Up @@ -557,6 +570,7 @@ const STRINGS = {
rightClick: 'Щёлкните правой кнопкой по слову →',
addTo: 'Добавить в Ordforråd',
delete: 'Удалить',
revalidate: 'Перепроверить с ИИ',
fetchTip: 'Обновить модели',
noModel: 'LM Studio: модель не загружена.',
unreachable: 'LM Studio недоступен. Запущен ли сервер?',
Expand Down Expand Up @@ -596,6 +610,7 @@ const STRINGS = {
rightClick: 'Bir kelimeye sağ tıklayın →',
addTo: 'Ordforråd\'a Ekle',
delete: 'Sil',
revalidate: 'Yapay zeka ile yeniden doğrula',
fetchTip: 'Modelleri yenile',
noModel: 'LM Studio: yüklü model yok.',
unreachable: 'LM Studio\'ya ulaşılamıyor. Sunucu çalışıyor mu?',
Expand Down Expand Up @@ -635,6 +650,7 @@ const STRINGS = {
rightClick: 'Kattintson jobb gombbal egy szóra →',
addTo: 'Hozzáadás az Ordforråd-hoz',
delete: 'Törlés',
revalidate: 'Újraellenőrzés AI-val',
fetchTip: 'Modellek újratöltése',
noModel: 'LM Studio: nincs betöltve modell.',
unreachable: 'LM Studio nem elérhető. Fut a szerver?',
Expand Down Expand Up @@ -674,6 +690,7 @@ const STRINGS = {
rightClick: 'Δεξί κλικ σε μια λέξη →',
addTo: 'Προσθήκη στο Ordforråd',
delete: 'Διαγραφή',
revalidate: 'Επανεπαλήθευση με ΑΙ',
fetchTip: 'Επαναφόρτωση μοντέλων',
noModel: 'LM Studio: δεν φορτώθηκε μοντέλο.',
unreachable: 'Το LM Studio δεν είναι προσβάσιμο. Τρέχει ο διακομιστής;',
Expand Down Expand Up @@ -713,6 +730,7 @@ const STRINGS = {
rightClick: 'Hægrismelltu á orð →',
addTo: 'Bæta við Ordforråd',
delete: 'Eyða',
revalidate: 'Endurstaðfesta með gervigreind',
fetchTip: 'Hlaða líkön aftur',
noModel: 'LM Studio: ekkert líkan hlaðið.',
unreachable: 'LM Studio er ekki aðgengilegt. Er þjónninn að keyra?',
Expand Down Expand Up @@ -752,6 +770,7 @@ const STRINGS = {
rightClick: '単語を右クリック →',
addTo: 'Ordforråd に追加',
delete: '削除',
revalidate: 'AIで再検証',
fetchTip: 'モデルを再読み込み',
noModel: 'LM Studio: モデルが読み込まれていません。',
unreachable: 'LM Studio に接続できません。サーバーは起動していますか?',
Expand Down Expand Up @@ -792,6 +811,7 @@ const STRINGS = {
rightClick: '단어를 우클릭 →',
addTo: 'Ordforråd에 추가',
delete: '삭제',
revalidate: 'AI로 재검증',
fetchTip: '모델 새로고침',
noModel: 'LM Studio: 로드된 모델이 없습니다.',
unreachable: 'LM Studio에 연결할 수 없습니다. 서버가 실행 중인가요?',
Expand Down Expand Up @@ -832,6 +852,7 @@ const STRINGS = {
rightClick: '右键单击单词 →',
addTo: '添加到Ordforråd',
delete: '删除',
revalidate: 'AI重新验证',
fetchTip: '重新加载模型',
noModel: 'LM Studio:未加载模型。',
unreachable: 'LM Studio无法连接。服务器是否正在运行?',
Expand Down Expand Up @@ -872,6 +893,7 @@ const STRINGS = {
rightClick: 'انقر بزر الماوس الأيمن على كلمة →',
addTo: 'إضافة إلى Ordforråd',
delete: 'حذف',
revalidate: 'إعادة التحقق بالذكاء الاصطناعي',
fetchTip: 'إعادة تحميل النماذج',
noModel: 'LM Studio: لم يتم تحميل نموذج.',
unreachable: 'LM Studio غير متاح. هل الخادم يعمل؟',
Expand Down
33 changes: 32 additions & 1 deletion src/popup/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ async function init() {
if (msg.type === 'CARD_ERROR') {
showErrorBanner(`${currentStrings.error}: ${msg.error}`);
}
if (msg.type === 'CARD_REVALIDATED') {
revalidatingIds.delete(msg.card.id);
getAllCards().then(cards => render(cards)).catch(() => {});
}
if (msg.type === 'CARD_REVALIDATE_ERROR') {
revalidatingIds.delete(msg.cardId);
showToast(`${currentStrings.error}: ${msg.error}`, true);
getAllCards().then(cards => render(cards)).catch(() => {});
}
});
}

Expand Down Expand Up @@ -271,6 +280,11 @@ elBtnSave.addEventListener('click', async () => {
showToast(currentStrings.saved);
});

// ── Revalidation state ────────────────────────────────────────────────────────

/** IDs of cards currently being revalidated by the AI */
const revalidatingIds = new Set();

// ── Render ────────────────────────────────────────────────────────────────────

function render(cards) {
Expand All @@ -295,7 +309,8 @@ function render(cards) {
}

function buildCard(card) {
const s = currentStrings;
const s = currentStrings;
const isRevalidating = revalidatingIds.has(card.id);
const li = document.createElement('li');
li.className = 'bg-white dark:bg-[#292a2d] rounded-xl border border-slate-200 dark:border-[#3c4043] shadow-sm overflow-hidden';
li.dataset.id = card.id;
Expand All @@ -306,6 +321,8 @@ function buildCard(card) {
<div class="flex items-baseline gap-2">
<span class="text-slate-900 dark:text-[#e8eaed] font-bold text-[15px] leading-snug grow">${esc(card.word)}</span>
<span class="text-[10px] text-slate-400 dark:text-[#9aa0a6] truncate max-w-[140px] text-right" title="${esc(card.wordClass)}">${esc(card.wordClass)}</span>
<button class="btn-revalidate text-slate-300 dark:text-[#5f6368] hover:text-blue-400 transition-colors text-sm leading-none shrink-0 cursor-pointer disabled:opacity-40 disabled:cursor-default"
data-id="${esc(card.id)}" title="${esc(s.revalidate)}"${isRevalidating ? ' disabled' : ''}>${isRevalidating ? '<span class="inline-block animate-spin">↻</span>' : '↻'}</button>
<button class="btn-delete text-slate-300 dark:text-[#5f6368] hover:text-rose-400 transition-colors text-sm leading-none shrink-0 cursor-pointer"
data-id="${esc(card.id)}" title="${esc(s.delete)}">✕</button>
</div>
Expand All @@ -329,6 +346,9 @@ function buildCard(card) {
</div>
`;

if (!isRevalidating) {
li.querySelector('.btn-revalidate').addEventListener('click', () => handleRevalidate(card));
}
li.querySelector('.btn-delete').addEventListener('click', () => handleDelete(card.id));
return li;
}
Expand Down Expand Up @@ -376,6 +396,17 @@ function removeSkeleton(word) {

elExport.addEventListener('click', handleExport);

async function handleRevalidate(card) {
revalidatingIds.add(card.id);
render(await getAllCards().catch(() => []));
chrome.runtime.sendMessage({
type: 'REVALIDATE_CARD',
cardId: card.id,
word: card.word,
context: card.context,
}).catch(() => {});
}

async function handleDelete(id) {
await deleteCard(id);
try {
Expand Down
Loading