Spaces:
Paused
Paused
Feat: Implement invisible pending queue for quick settings modal to allow background processing without blocking UI
Browse files- index.html +1 -1
- src/app.js +13 -24
- src/db/database.js +6 -2
- src/views/vocabulary-modal.js +10 -41
index.html
CHANGED
|
@@ -74,7 +74,7 @@
|
|
| 74 |
</head>
|
| 75 |
<body class="dark-theme in-chat-view" style="visibility: hidden;">
|
| 76 |
|
| 77 |
-
<div id="live-version-badge" style="position: fixed; top: 50px; left: 50%; transform: translateX(-50%); font-size: 10px; color: var(--text-secondary); opacity: 0.6; z-index: 99999; pointer-events: none; font-weight: 500; font-family: monospace; letter-spacing: 1px; background: rgba(0,0,0,0.2); padding: 2px 8px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.05);">BUILD 1.8.
|
| 78 |
|
| 79 |
<!-- SVG Filters for Liquid Glass Refraction -->
|
| 80 |
<svg width="0" height="0" style="position: absolute; pointer-events: none;">
|
|
|
|
| 74 |
</head>
|
| 75 |
<body class="dark-theme in-chat-view" style="visibility: hidden;">
|
| 76 |
|
| 77 |
+
<div id="live-version-badge" style="position: fixed; top: 50px; left: 50%; transform: translateX(-50%); font-size: 10px; color: var(--text-secondary); opacity: 0.6; z-index: 99999; pointer-events: none; font-weight: 500; font-family: monospace; letter-spacing: 1px; background: rgba(0,0,0,0.2); padding: 2px 8px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.05);">BUILD 1.8.322</div>
|
| 78 |
|
| 79 |
<!-- SVG Filters for Liquid Glass Refraction -->
|
| 80 |
<svg width="0" height="0" style="position: absolute; pointer-events: none;">
|
src/app.js
CHANGED
|
@@ -365,31 +365,20 @@ Zu übersetzen: "${word}"`;
|
|
| 365 |
// Only run if we're not in the tiny transparent overlay mode
|
| 366 |
if (window.isOverlayMode) return;
|
| 367 |
|
| 368 |
-
let
|
| 369 |
-
if (!
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
if (cardsToEnrich.length > 0) {
|
| 381 |
-
console.log(`Found ${cardsToEnrich.length} cards needing AI enrichment...`);
|
| 382 |
-
|
| 383 |
-
// Delay slightly to not block initial UI render
|
| 384 |
-
setTimeout(async () => {
|
| 385 |
-
try {
|
| 386 |
-
const { aiRouter } = await import('./ai/router.js');
|
| 387 |
|
| 388 |
-
|
| 389 |
-
const word = card.front;
|
| 390 |
-
const pureTranslation = card.back.replace('<!-- NEEDS_ENRICHMENT -->', '').trim();
|
| 391 |
-
|
| 392 |
-
const prompt = `Erstelle eine kurze Erklärung und ein passendes Beispiel für diese Vokabelkarte, sowie einen Suchbegriff für ein Bild.
|
| 393 |
Wort: "${word}"
|
| 394 |
Übersetzung: "${pureTranslation}"
|
| 395 |
|
|
|
|
| 365 |
// Only run if we're not in the tiny transparent overlay mode
|
| 366 |
if (window.isOverlayMode) return;
|
| 367 |
|
| 368 |
+
let pendingCards = db.getPendingCards();
|
| 369 |
+
if (!pendingCards || pendingCards.length === 0) return;
|
| 370 |
+
|
| 371 |
+
if (!window.isEnriching) {
|
| 372 |
+
window.isEnriching = true;
|
| 373 |
+
try {
|
| 374 |
+
const { aiRouter } = await import('./ai/router.js');
|
| 375 |
+
|
| 376 |
+
// Process sequentially
|
| 377 |
+
for (const card of pendingCards) {
|
| 378 |
+
const word = card.front;
|
| 379 |
+
const pureTranslation = card.back.replace('<!-- PENDING_CORRECTION -->', '').replace('<!-- NEEDS_ENRICHMENT -->', '').trim();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
|
| 381 |
+
const prompt = `Erstelle eine kurze Erklärung und ein passendes Beispiel für diese Vokabelkarte, sowie einen Suchbegriff für ein Bild.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
Wort: "${word}"
|
| 383 |
Übersetzung: "${pureTranslation}"
|
| 384 |
|
src/db/database.js
CHANGED
|
@@ -504,7 +504,7 @@ class Database {
|
|
| 504 |
getDueCards(deckId) {
|
| 505 |
const now = new Date().toISOString();
|
| 506 |
return this.exec(
|
| 507 |
-
"SELECT * FROM flashcards WHERE deck_id = ? AND next_review <= ? ORDER BY next_review ASC",
|
| 508 |
[deckId, now]
|
| 509 |
);
|
| 510 |
}
|
|
@@ -519,11 +519,15 @@ class Database {
|
|
| 519 |
|
| 520 |
getCardsForDeck(deckId) {
|
| 521 |
return this.exec(
|
| 522 |
-
"SELECT * FROM flashcards WHERE deck_id = ? ORDER BY created_at DESC",
|
| 523 |
[deckId]
|
| 524 |
);
|
| 525 |
}
|
| 526 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 527 |
searchFlashcards(query) {
|
| 528 |
return this.exec(`
|
| 529 |
SELECT f.*, d.title as deck_title, d.color as deck_color
|
|
|
|
| 504 |
getDueCards(deckId) {
|
| 505 |
const now = new Date().toISOString();
|
| 506 |
return this.exec(
|
| 507 |
+
"SELECT * FROM flashcards WHERE deck_id = ? AND next_review <= ? AND back NOT LIKE '%<!-- PENDING_CORRECTION -->%' ORDER BY next_review ASC",
|
| 508 |
[deckId, now]
|
| 509 |
);
|
| 510 |
}
|
|
|
|
| 519 |
|
| 520 |
getCardsForDeck(deckId) {
|
| 521 |
return this.exec(
|
| 522 |
+
"SELECT * FROM flashcards WHERE deck_id = ? AND back NOT LIKE '%<!-- PENDING_CORRECTION -->%' ORDER BY created_at DESC",
|
| 523 |
[deckId]
|
| 524 |
);
|
| 525 |
}
|
| 526 |
|
| 527 |
+
getPendingCards() {
|
| 528 |
+
return this.exec("SELECT * FROM flashcards WHERE back LIKE '%<!-- PENDING_CORRECTION -->%' OR back LIKE '%<!-- NEEDS_ENRICHMENT -->%'");
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
searchFlashcards(query) {
|
| 532 |
return this.exec(`
|
| 533 |
SELECT f.*, d.title as deck_title, d.color as deck_color
|
src/views/vocabulary-modal.js
CHANGED
|
@@ -271,49 +271,18 @@ Nur das rohe JSON:`;
|
|
| 271 |
const isDuplicate = existingCards.some(card => card.front.toLowerCase() === parsed.front.toLowerCase());
|
| 272 |
|
| 273 |
if (!isDuplicate) {
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
Wort: "${parsed.front}"
|
| 281 |
-
Übersetzung: "${parsed.back}"
|
| 282 |
-
|
| 283 |
-
WICHTIG: Prüfe, ob das "Wort" offensichtlich falsch geschrieben ist (z.B. phonetisch nach Gehör geschrieben).
|
| 284 |
-
Falls es einen Tippfehler enthält, gib das richtig geschriebene Wort im Feld "correctedWord" zurück. Wenn es korrekt geschrieben ist, lass das Feld weg oder setze es auf null.
|
| 285 |
-
|
| 286 |
-
Generiere:
|
| 287 |
-
- "explanation": 1-2 kurze Sätze in der Sprache der Übersetzung, die genau dieses Wort erklären. WICHTIG: Markiere das Lernwort (und seine Abwandlungen) IMMER fett mit HTML-Tags (z.B. <b>Wort</b>).
|
| 288 |
-
- "example": Ein prägnanter Beispielsatz mit der Übersetzung. Das Lernwort MUSS auch hier mit <b>...</b> fett markiert sein.
|
| 289 |
-
- "imageQuery": Ein präziser englischer Suchbegriff für die Bing-Bildersuche. Das Bild muss EINDEUTIG zur Übersetzung passen. Füge IMMER 'single photo, clear focus, high quality' am Ende hinzu. Keine abstrakten Cartoons.
|
| 290 |
-
|
| 291 |
-
Antworte AUSSCHLIESSLICH mit einem validen JSON-Objekt. Keine Markdown-Formatierung.
|
| 292 |
-
Beispiel für Tippfehler bei "abanden":
|
| 293 |
-
{"correctedWord": "abandon", "explanation": "Jemanden oder etwas <b>verlassen</b> oder aufgeben.", "example": "Er musste das alte Auto <b>verlassen</b>.", "imageQuery": "abandoned old car single photo clear focus"}`;
|
| 294 |
-
|
| 295 |
-
const response = await aiRouter.sendMessage(prompt, {
|
| 296 |
-
model: 'gemini-3.1-flash-lite'
|
| 297 |
-
});
|
| 298 |
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
else if (jsonStr.startsWith('```')) jsonStr = jsonStr.replace(/```/g, '').trim();
|
| 302 |
-
|
| 303 |
-
const aiData = JSON.parse(jsonStr);
|
| 304 |
-
const finalFront = aiData.correctedWord ? aiData.correctedWord : parsed.front;
|
| 305 |
-
const finalBack = `${parsed.back}\n\n**Erklärung:** ${aiData.explanation}\n\n**Beispiel:** ${aiData.example}`;
|
| 306 |
-
const searchTerms = encodeURIComponent(aiData.imageQuery || finalFront);
|
| 307 |
-
const imageUrl = `https://tse1.mm.bing.net/th?q=${searchTerms}&w=800&h=1000&c=7&rs=1`;
|
| 308 |
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
} catch (e) {
|
| 312 |
-
console.warn("Background AI enrichment failed, saving original.", e);
|
| 313 |
-
db.addFlashcard(schnelldeck.id, parsed.front, parsed.back, '');
|
| 314 |
-
window.dispatchEvent(new CustomEvent('flashcards-updated'));
|
| 315 |
-
}
|
| 316 |
-
}, 100);
|
| 317 |
}
|
| 318 |
|
| 319 |
// Show success with translation
|
|
|
|
| 271 |
const isDuplicate = existingCards.some(card => card.front.toLowerCase() === parsed.front.toLowerCase());
|
| 272 |
|
| 273 |
if (!isDuplicate) {
|
| 274 |
+
let finalBack = parsed.back;
|
| 275 |
+
|
| 276 |
+
if (parsed.isDeepL) {
|
| 277 |
+
// Add a temporary marker so the background job knows this needs correction and enrichment
|
| 278 |
+
finalBack = `${parsed.back}\n\n<!-- PENDING_CORRECTION -->`;
|
| 279 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
|
| 281 |
+
const searchTerms = encodeURIComponent(parsed.imageQuery || parsed.front);
|
| 282 |
+
const imageUrl = `https://tse1.mm.bing.net/th?q=${searchTerms}&w=800&h=1000&c=7&rs=1`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
|
| 284 |
+
db.addFlashcard(schnelldeck.id, parsed.front, finalBack, imageUrl);
|
| 285 |
+
window.dispatchEvent(new CustomEvent('flashcards-updated'));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
}
|
| 287 |
|
| 288 |
// Show success with translation
|