diff --git a/backend/lernen.py b/backend/lernen.py
index f525e73..b8802fb 100644
--- a/backend/lernen.py
+++ b/backend/lernen.py
@@ -213,7 +213,7 @@ async def _frage_mit_kritik(
return None
frage = data["frage"]
probleme = await _kritik_call(
- "Baustein-Frage-Kritik", provider,
+ "Baustein-Frage-Kritik", provider, role="guide", # starke KI prüft die Regeln
topic=topic, baustein=baustein, section_block=section_block,
vertiefung_block=vertiefung_block, transcript=transcript, frage=frage,
)
diff --git a/frontend/src/components/BausteinFokus.vue b/frontend/src/components/BausteinFokus.vue
index 51279e2..5f15edb 100644
--- a/frontend/src/components/BausteinFokus.vue
+++ b/frontend/src/components/BausteinFokus.vue
@@ -19,20 +19,41 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
const emit = defineEmits(['close', 'prev', 'next', 'statusChanged'])
-const guideEl = ref(null) // linke Guide-Spalte (Scroll-Ziel für ↑/↓)
+const guideEl = ref(null) // linke Guide-Spalte (Scroll-Ziel für ALT+↑/↓)
+const rightEl = ref(null) // rechte Spalte (Prüfungs-Panel mit Eingabefeld)
-// Die Fokus-Ansicht besitzt die Pfeiltasten — auch bei fokussiertem Eingabefeld.
-// ←/→ blättern Baustein, ↑/↓ scrollen den Guide; Browser-Default wird unterdrückt.
-// ALT/Strg/Meta ignorieren (ALT+1/2/3 gehören dem Panel).
-function onKey(e) {
- if (e.altKey || e.ctrlKey || e.metaKey) return
- if (e.key === 'ArrowLeft') { e.preventDefault(); if (props.hasPrev) emit('prev') }
- else if (e.key === 'ArrowRight') { e.preventDefault(); if (props.hasNext) emit('next') }
+// ALT ist der Modifier der Fokus-Ansicht. Plain Pfeile bleiben Browser-Standard.
+// ALT+←/→ blättert Baustein, ALT+↑/↓ scrollt den Guide. Ein reiner ALT-Tipp
+// (drücken+loslassen ohne andere Taste) toggelt den Fokus aufs Eingabefeld.
+let altAlone = false
+function onKeyDown(e) {
+ if (e.key === 'Alt') { altAlone = true; e.preventDefault(); return } // unterdrückt Firefox-Menüleiste
+ if (e.ctrlKey || e.metaKey || !e.altKey) return // plain/andere → normal
+ altAlone = false // ALT+irgendwas = kein Lone-Tap
+ if (e.key === 'ArrowLeft') { e.preventDefault(); if (!e.repeat && props.hasPrev) emit('prev') }
+ else if (e.key === 'ArrowRight') { e.preventDefault(); if (!e.repeat && props.hasNext) emit('next') }
else if (e.key === 'ArrowUp') { e.preventDefault(); guideEl.value?.scrollBy(0, -120) }
else if (e.key === 'ArrowDown') { e.preventDefault(); guideEl.value?.scrollBy(0, 120) }
}
-onMounted(() => window.addEventListener('keydown', onKey))
-onUnmounted(() => window.removeEventListener('keydown', onKey))
+function onKeyUp(e) {
+ if (e.key === 'Alt') { e.preventDefault(); if (altAlone) { altAlone = false; toggleInput() } }
+}
+function resetAlt() { altAlone = false } // Fenster-Blur (ALT+Tab) → kein falscher Tipp
+function toggleInput() {
+ const el = rightEl.value?.querySelector('textarea')
+ if (!el) return
+ document.activeElement === el ? el.blur() : el.focus()
+}
+onMounted(() => {
+ window.addEventListener('keydown', onKeyDown)
+ window.addEventListener('keyup', onKeyUp)
+ window.addEventListener('blur', resetAlt)
+})
+onUnmounted(() => {
+ window.removeEventListener('keydown', onKeyDown)
+ window.removeEventListener('keyup', onKeyUp)
+ window.removeEventListener('blur', resetAlt)
+})
const pct = (n) => (100 * n / (props.fortschritt.total || 1)) + '%'
@@ -69,7 +90,7 @@ const standFarbe = computed(() => {
-
+
m.kind === 'frage').map((m) => normFrage(m.content)))
}
-// Holt eine Frage; bis zu 3 Versuche, eine NICHT wiederholte zu bekommen. Sonst best-effort die letzte.
-async function frageHolen() {
- const gestellt = gestellteFragen()
- let letzte = null
- for (let i = 0; i < 3; i++) {
- const res = await pruefeBaustein({
- topic: props.topic, baustein: props.baustein, section: props.section,
- provider: props.provider, aktion: 'frage', messages: pruefDialog(),
- })
- letzte = res.frage
- if (!gestellt.has(normFrage(res.frage))) return res.frage
- }
- return letzte
+// --- Fragen-Pool: ZIEL vorgenerierte, geprüfte, unbeantwortete Fragen je Baustein (im Slot) ---
+const POOL_ZIEL = 5
+const pool = slot.pool
+const inflight = slot.inflight
+let poolMiss = 0 // aufeinanderfolgende Dubletten/Fehler — Cap gegen Endlosschleife (kleine Bausteine)
+
+function istBekannt(text) {
+ const n = normFrage(text)
+ return !n || gestellteFragen().has(n) || pool.value.some((f) => normFrage(f) === n)
}
-// Nächste Frage im Hintergrund vorbereiten (überbrückt die Wartezeit). Ohne pruefLoading/pruefRun.
-function prefetchFrage() {
- if (naechsteFrage.value || prefetchPromise) return
- prefetchPromise = frageHolen()
- .then((text) => { naechsteFrage.value = text })
- .catch(() => {})
- .finally(() => { prefetchPromise = null })
-}
-
-// Erste Frage (Phase idle): frisch generieren, dann die nächste vorbereiten.
-function frageAnfordern() {
- if (pruefLoading.value) return
- pruefSenden({ aktion: 'frage' }, (res) => {
- frageZeigen(res.frage)
- prefetchFrage()
+async function frageEinzeln() {
+ const res = await pruefeBaustein({
+ topic: props.topic, baustein: props.baustein, section: props.section,
+ provider: props.provider, aktion: 'frage', messages: pruefDialog(),
})
+ return res.frage
}
-// „Nächste Frage": vorgemerkte sofort zeigen, sonst auf den Prefetch warten, sonst frisch holen.
+// Pool parallel auf ZIEL auffüllen; jede Auflösung dedupt + füllt nach.
+function poolFuellen() {
+ while (pool.value.length + inflight.value < POOL_ZIEL && poolMiss < 6) {
+ inflight.value++
+ frageEinzeln()
+ .then((text) => {
+ if (text && !istBekannt(text)) { pool.value.push(text); poolMiss = 0 }
+ else poolMiss++
+ })
+ .catch(() => { poolMiss++ })
+ .finally(() => { inflight.value--; poolFuellen() })
+ }
+}
+
+// Eine Frage aus dem Pool nehmen; ist er leer, auf die erste eintreffende warten.
+async function poolNehmen() {
+ if (!pool.value.length) {
+ pruefLoading.value = true
+ try {
+ while (!pool.value.length && (inflight.value > 0 || poolMiss < 6)) {
+ if (!inflight.value) poolFuellen()
+ await new Promise((r) => setTimeout(r, 120))
+ }
+ } finally { pruefLoading.value = false }
+ }
+ return pool.value.length ? pool.value.shift() : null
+}
+
+// Erste Frage (Phase idle): ZIEL parallel anstoßen, schnellste zuerst zeigen.
+async function frageAnfordern() {
+ if (pruefLoading.value) return
+ poolMiss = 0
+ poolFuellen()
+ const text = await poolNehmen()
+ if (text) {
+ frageZeigen(text)
+ nextTick(() => pruefInputEl.value?.focus())
+ } else {
+ pruefMessages.value.push({ role: 'assistant', kind: 'fehler', content: 'Keine Frage — bitte erneut.' })
+ }
+ poolFuellen()
+}
+
+// „Nächste Frage": sofort aus dem Pool, dann nachfüllen.
async function naechsteFrageZeigen() {
if (pruefLoading.value) return
- if (!naechsteFrage.value && prefetchPromise) {
- pruefLoading.value = true
- try { await prefetchPromise } finally { pruefLoading.value = false }
- }
- if (naechsteFrage.value) {
- const text = naechsteFrage.value
- naechsteFrage.value = null
+ const text = await poolNehmen()
+ if (text) {
frageZeigen(text)
pruefScroll()
nextTick(() => pruefInputEl.value?.focus())
- prefetchFrage()
- return
- }
- // Fallback: kein Cache → frisch holen (mit Dedup).
- pruefLoading.value = true
- try {
- const text = await frageHolen()
- if (text) {
- frageZeigen(text)
- pruefScroll()
- nextTick(() => pruefInputEl.value?.focus())
- prefetchFrage()
- } else {
- pruefMessages.value.push({ role: 'assistant', kind: 'fehler', content: 'Keine neue Frage — bitte erneut.' })
- }
- } catch {
- pruefMessages.value.push({ role: 'assistant', kind: 'fehler', content: 'Hat nicht geklappt — bitte erneut.' })
- } finally {
- pruefLoading.value = false
+ } else {
+ pruefMessages.value.push({ role: 'assistant', kind: 'fehler', content: 'Keine neue Frage — bitte erneut.' })
}
+ poolFuellen()
}
function nachfragen() {
diff --git a/frontend/src/pruefungCache.js b/frontend/src/pruefungCache.js
index 965b072..8f63028 100644
--- a/frontend/src/pruefungCache.js
+++ b/frontend/src/pruefungCache.js
@@ -6,7 +6,7 @@
import { ref } from 'vue'
-const store = new Map() // key "topic::baustein" -> { messages, phase, aktuelleFrage, letztesFeedback, scoreVorFrage, streakVorFrage }
+const store = new Map() // key "topic::baustein" -> Refs (Verlauf + Fragen-Pool)
export function usePruefSlot(key) {
if (!store.has(key)) {
@@ -17,6 +17,8 @@ export function usePruefSlot(key) {
letztesFeedback: ref(''),
scoreVorFrage: ref(0),
streakVorFrage: ref(0),
+ pool: ref([]), // vorgenerierte, geprüfte, unbeantwortete Fragen
+ inflight: ref(0), // laufende Pool-Generierungen (instanzübergreifend koordiniert)
})
}
return store.get(key)