Phase 9i: Cloze-Hint-Anzeige
renderClozePrompt zeigt jetzt den Hint im aktiven Cluster anstelle
von „…", wenn der User die `{{c1::Antwort::Hinweis}}`-Syntax nutzt.
Beispiel: Prompt für `{{c1::Paris::Hauptstadt}}` wird "[Hauptstadt]"
statt "[…]". Nicht-aktive Cluster expandieren auf ihre Antwort —
der Hint bleibt unsichtbar, bis sein Cluster dran ist.
Neue Helper-Funktion hintForCluster(text, clusterId) liefert die
erste Hint-Annotation eines Clusters (deterministisches Verhalten
bei mehreren `{{c1::…}}`-Vorkommen mit unterschiedlichen Hints).
5 neue Tests in cloze.test.ts: hintForCluster (4 Cases), erweiterte
renderClozePrompt-Cases. Domain jetzt 46 Tests grün.
cloze_help in i18n DE/EN um die Hint-Syntax-Erklärung erweitert.
Live-Preview im Card-New/Edit nutzt die erweiterte Logik automatisch
(beide rufen renderClozePrompt aus @cards/domain).
svelte-check 379 files 0 errors, API-Tests unverändert grün
(48/9), Web-Tests 5/1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fd86d968a4
commit
4b451f1b8d
4 changed files with 60 additions and 12 deletions
|
|
@ -1,17 +1,24 @@
|
|||
/**
|
||||
* Cloze-Helpers: Cluster-Extraktion + Sub-Index-Mapping.
|
||||
* Cloze-Helpers: Cluster-Extraktion + Sub-Index-Mapping + Render.
|
||||
*
|
||||
* Anki-Cloze-Markup: `{{cN::answer}}` oder `{{cN::answer::hint}}`. N ist
|
||||
* eine 1-basierte Cluster-ID — alle Vorkommen mit gleichem N gehören zum
|
||||
* selben Cluster (= ein Review). Mehrere Cluster pro Karte → mehrere
|
||||
* Sub-Index-Reviews.
|
||||
*
|
||||
* Hint (`::hint`) wird MVP-stumm fallengelassen — Anki zeigt ihn unter
|
||||
* der Maske, das ist Polish-Phase.
|
||||
* Hint-Support: ein optionaler dritter Slot (`::hint`) liefert einen
|
||||
* kurzen Tipp, der im Prompt unter der Maske erscheint. In der Antwort
|
||||
* wird der Hint nicht erneut angezeigt — der User hat die Antwort schon.
|
||||
*/
|
||||
|
||||
const CLUSTER_RE = /\{\{c(\d+)::([^}]*?)(?:::([^}]*?))?\}\}/g;
|
||||
|
||||
export interface ClozeCluster {
|
||||
id: number;
|
||||
answer: string;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert distinct Cluster-IDs (sortiert). Aus `{{c2::a}} {{c1::b}} {{c2::c}}`
|
||||
* wird `[1, 2]`. Leeres Result, wenn kein Cluster-Markup im Text.
|
||||
|
|
@ -25,6 +32,19 @@ export function extractClusterIds(text: string): number[] {
|
|||
return [...ids].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert für einen Cluster den ersten gefundenen Hint, falls einer
|
||||
* vorhanden ist. Mehrere Vorkommen desselben Clusters (z.B. `{{c1::a}}`
|
||||
* und `{{c1::b::tip}}`) — die erste Hint-Annotation gewinnt.
|
||||
*/
|
||||
export function hintForCluster(text: string, clusterId: number): string | undefined {
|
||||
for (const match of text.matchAll(CLUSTER_RE)) {
|
||||
const n = Number(match[1]);
|
||||
if (n === clusterId && match[3]) return match[3];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wie viele Reviews braucht eine Cloze-Karte? Ein Review pro distinct
|
||||
* Cluster-ID. Gibt 0 zurück, wenn der Text gar kein Cluster-Markup enthält
|
||||
|
|
@ -45,14 +65,17 @@ export function clusterIdForSubIndex(text: string, subIndex: number): number | n
|
|||
}
|
||||
|
||||
/**
|
||||
* Render-Helper für den Prompt: aktiver Cluster wird zu `[…]`, alle
|
||||
* anderen Cluster werden auf ihre Antwort expandiert. Hint (`::hint`)
|
||||
* wird gedroppt.
|
||||
* Render-Helper für den Prompt: aktiver Cluster wird zu `[…]` (oder
|
||||
* `[hint]`, falls ein Hint annotiert ist), alle anderen Cluster werden
|
||||
* auf ihre Antwort expandiert.
|
||||
*/
|
||||
export function renderClozePrompt(text: string, activeClusterId: number): string {
|
||||
return text.replace(CLUSTER_RE, (_, nStr: string, answer: string) => {
|
||||
return text.replace(CLUSTER_RE, (_, nStr: string, answer: string, hint?: string) => {
|
||||
const n = Number(nStr);
|
||||
return n === activeClusterId ? '[…]' : answer;
|
||||
if (n === activeClusterId) {
|
||||
return hint ? `[${hint}]` : '[…]';
|
||||
}
|
||||
return answer;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest';
|
|||
|
||||
import {
|
||||
extractClusterIds,
|
||||
hintForCluster,
|
||||
subIndexCountForCloze,
|
||||
clusterIdForSubIndex,
|
||||
renderClozePrompt,
|
||||
|
|
@ -54,15 +55,39 @@ describe('clusterIdForSubIndex', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('hintForCluster', () => {
|
||||
it('liefert den Hint, wenn vorhanden', () => {
|
||||
expect(hintForCluster('Die {{c1::Antwort::Hinweis}}.', 1)).toBe('Hinweis');
|
||||
});
|
||||
|
||||
it('liefert undefined ohne Hint-Annotation', () => {
|
||||
expect(hintForCluster('Die {{c1::Antwort}}.', 1)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('liefert undefined für nicht-existierende Cluster', () => {
|
||||
expect(hintForCluster('Die {{c1::Antwort::Hint}}.', 2)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('erste Hint-Annotation gewinnt bei mehreren Vorkommen', () => {
|
||||
const text = '{{c1::a}} und {{c1::b::erster}} und {{c1::c::zweiter}}';
|
||||
expect(hintForCluster(text, 1)).toBe('erster');
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderClozePrompt', () => {
|
||||
it('maskiert aktiven Cluster, expandiert andere', () => {
|
||||
const out = renderClozePrompt('{{c1::A}} und {{c2::B}}', 1);
|
||||
expect(out).toBe('[…] und B');
|
||||
});
|
||||
|
||||
it('droppt Hint beim Maskieren', () => {
|
||||
it('zeigt Hint im aktiven Cluster, wenn vorhanden', () => {
|
||||
const out = renderClozePrompt('Die {{c1::Antwort::Hinweis}}.', 1);
|
||||
expect(out).toBe('Die […].');
|
||||
expect(out).toBe('Die [Hinweis].');
|
||||
});
|
||||
|
||||
it('expandiert nicht-aktiven Cluster auch wenn Hint vorhanden', () => {
|
||||
const out = renderClozePrompt('{{c1::A::tipp}} und {{c2::B}}', 2);
|
||||
expect(out).toBe('A und […]');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue