update
This commit is contained in:
@@ -213,7 +213,7 @@ async def _frage_mit_kritik(
|
|||||||
return None
|
return None
|
||||||
frage = data["frage"]
|
frage = data["frage"]
|
||||||
probleme = await _kritik_call(
|
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,
|
topic=topic, baustein=baustein, section_block=section_block,
|
||||||
vertiefung_block=vertiefung_block, transcript=transcript, frage=frage,
|
vertiefung_block=vertiefung_block, transcript=transcript, frage=frage,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,20 +19,41 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|||||||
|
|
||||||
const emit = defineEmits(['close', 'prev', 'next', 'statusChanged'])
|
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.
|
// ALT ist der Modifier der Fokus-Ansicht. Plain Pfeile bleiben Browser-Standard.
|
||||||
// ←/→ blättern Baustein, ↑/↓ scrollen den Guide; Browser-Default wird unterdrückt.
|
// ALT+←/→ blättert Baustein, ALT+↑/↓ scrollt den Guide. Ein reiner ALT-Tipp
|
||||||
// ALT/Strg/Meta ignorieren (ALT+1/2/3 gehören dem Panel).
|
// (drücken+loslassen ohne andere Taste) toggelt den Fokus aufs Eingabefeld.
|
||||||
function onKey(e) {
|
let altAlone = false
|
||||||
if (e.altKey || e.ctrlKey || e.metaKey) return
|
function onKeyDown(e) {
|
||||||
if (e.key === 'ArrowLeft') { e.preventDefault(); if (props.hasPrev) emit('prev') }
|
if (e.key === 'Alt') { altAlone = true; e.preventDefault(); return } // unterdrückt Firefox-Menüleiste
|
||||||
else if (e.key === 'ArrowRight') { e.preventDefault(); if (props.hasNext) emit('next') }
|
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 === 'ArrowUp') { e.preventDefault(); guideEl.value?.scrollBy(0, -120) }
|
||||||
else if (e.key === 'ArrowDown') { e.preventDefault(); guideEl.value?.scrollBy(0, 120) }
|
else if (e.key === 'ArrowDown') { e.preventDefault(); guideEl.value?.scrollBy(0, 120) }
|
||||||
}
|
}
|
||||||
onMounted(() => window.addEventListener('keydown', onKey))
|
function onKeyUp(e) {
|
||||||
onUnmounted(() => window.removeEventListener('keydown', onKey))
|
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)) + '%'
|
const pct = (n) => (100 * n / (props.fortschritt.total || 1)) + '%'
|
||||||
|
|
||||||
@@ -69,7 +90,7 @@ const standFarbe = computed(() => {
|
|||||||
<div ref="guideEl" class="fokus-col left">
|
<div ref="guideEl" class="fokus-col left">
|
||||||
<div class="markdown" v-html="renderMarkdown(baustein.md)"></div>
|
<div class="markdown" v-html="renderMarkdown(baustein.md)"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fokus-col right">
|
<div ref="rightEl" class="fokus-col right">
|
||||||
<BausteinPanel
|
<BausteinPanel
|
||||||
mode="full"
|
mode="full"
|
||||||
:key="baustein.title + '|' + tab"
|
:key="baustein.title + '|' + tab"
|
||||||
|
|||||||
@@ -94,11 +94,9 @@ const aktuelleFrage = slot.aktuelleFrage // ankert Bewertung/Diskussion
|
|||||||
const letztesFeedback = slot.letztesFeedback // Kontext für die Diskussion über eine Bewertung
|
const letztesFeedback = slot.letztesFeedback // Kontext für die Diskussion über eine Bewertung
|
||||||
const scoreVorFrage = slot.scoreVorFrage // Score, als die Frage gestellt wurde → driftfreies (Re-)Bewerten
|
const scoreVorFrage = slot.scoreVorFrage // Score, als die Frage gestellt wurde → driftfreies (Re-)Bewerten
|
||||||
const streakVorFrage = slot.streakVorFrage // Streak, als die Frage gestellt wurde (driftfrei)
|
const streakVorFrage = slot.streakVorFrage // Streak, als die Frage gestellt wurde (driftfrei)
|
||||||
// Transient (pro Instanz): Eingabe-Entwurf, Lade-Spinner, Prefetch.
|
// Transient (pro Instanz): Eingabe-Entwurf, Lade-Spinner.
|
||||||
const pruefInput = ref('')
|
const pruefInput = ref('')
|
||||||
const pruefLoading = ref(false)
|
const pruefLoading = ref(false)
|
||||||
const naechsteFrage = ref(null) // im Hintergrund vorbereitete nächste Frage (Prefetch)
|
|
||||||
let prefetchPromise = null // laufender Hintergrund-Abruf (verhindert Doppel-Prefetch)
|
|
||||||
|
|
||||||
const pruefMessagesEl = ref(null)
|
const pruefMessagesEl = ref(null)
|
||||||
const pruefInputEl = ref(null)
|
const pruefInputEl = ref(null)
|
||||||
@@ -164,72 +162,80 @@ function gestellteFragen() {
|
|||||||
return new Set(pruefMessages.value.filter((m) => m.kind === 'frage').map((m) => normFrage(m.content)))
|
return new Set(pruefMessages.value.filter((m) => 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.
|
// --- Fragen-Pool: ZIEL vorgenerierte, geprüfte, unbeantwortete Fragen je Baustein (im Slot) ---
|
||||||
async function frageHolen() {
|
const POOL_ZIEL = 5
|
||||||
const gestellt = gestellteFragen()
|
const pool = slot.pool
|
||||||
let letzte = null
|
const inflight = slot.inflight
|
||||||
for (let i = 0; i < 3; i++) {
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function frageEinzeln() {
|
||||||
const res = await pruefeBaustein({
|
const res = await pruefeBaustein({
|
||||||
topic: props.topic, baustein: props.baustein, section: props.section,
|
topic: props.topic, baustein: props.baustein, section: props.section,
|
||||||
provider: props.provider, aktion: 'frage', messages: pruefDialog(),
|
provider: props.provider, aktion: 'frage', messages: pruefDialog(),
|
||||||
})
|
})
|
||||||
letzte = res.frage
|
return res.frage
|
||||||
if (!gestellt.has(normFrage(res.frage))) return res.frage
|
|
||||||
}
|
|
||||||
return letzte
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nächste Frage im Hintergrund vorbereiten (überbrückt die Wartezeit). Ohne pruefLoading/pruefRun.
|
// Pool parallel auf ZIEL auffüllen; jede Auflösung dedupt + füllt nach.
|
||||||
function prefetchFrage() {
|
function poolFuellen() {
|
||||||
if (naechsteFrage.value || prefetchPromise) return
|
while (pool.value.length + inflight.value < POOL_ZIEL && poolMiss < 6) {
|
||||||
prefetchPromise = frageHolen()
|
inflight.value++
|
||||||
.then((text) => { naechsteFrage.value = text })
|
frageEinzeln()
|
||||||
.catch(() => {})
|
.then((text) => {
|
||||||
.finally(() => { prefetchPromise = null })
|
if (text && !istBekannt(text)) { pool.value.push(text); poolMiss = 0 }
|
||||||
}
|
else poolMiss++
|
||||||
|
|
||||||
// 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()
|
|
||||||
})
|
})
|
||||||
|
.catch(() => { poolMiss++ })
|
||||||
|
.finally(() => { inflight.value--; poolFuellen() })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// „Nächste Frage": vorgemerkte sofort zeigen, sonst auf den Prefetch warten, sonst frisch holen.
|
// Eine Frage aus dem Pool nehmen; ist er leer, auf die erste eintreffende warten.
|
||||||
async function naechsteFrageZeigen() {
|
async function poolNehmen() {
|
||||||
if (pruefLoading.value) return
|
if (!pool.value.length) {
|
||||||
if (!naechsteFrage.value && prefetchPromise) {
|
|
||||||
pruefLoading.value = true
|
|
||||||
try { await prefetchPromise } finally { pruefLoading.value = false }
|
|
||||||
}
|
|
||||||
if (naechsteFrage.value) {
|
|
||||||
const text = naechsteFrage.value
|
|
||||||
naechsteFrage.value = null
|
|
||||||
frageZeigen(text)
|
|
||||||
pruefScroll()
|
|
||||||
nextTick(() => pruefInputEl.value?.focus())
|
|
||||||
prefetchFrage()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Fallback: kein Cache → frisch holen (mit Dedup).
|
|
||||||
pruefLoading.value = true
|
pruefLoading.value = true
|
||||||
try {
|
try {
|
||||||
const text = await frageHolen()
|
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
|
||||||
|
const text = await poolNehmen()
|
||||||
if (text) {
|
if (text) {
|
||||||
frageZeigen(text)
|
frageZeigen(text)
|
||||||
pruefScroll()
|
pruefScroll()
|
||||||
nextTick(() => pruefInputEl.value?.focus())
|
nextTick(() => pruefInputEl.value?.focus())
|
||||||
prefetchFrage()
|
|
||||||
} else {
|
} else {
|
||||||
pruefMessages.value.push({ role: 'assistant', kind: 'fehler', content: 'Keine neue Frage — bitte erneut.' })
|
pruefMessages.value.push({ role: 'assistant', kind: 'fehler', content: 'Keine neue Frage — bitte erneut.' })
|
||||||
}
|
}
|
||||||
} catch {
|
poolFuellen()
|
||||||
pruefMessages.value.push({ role: 'assistant', kind: 'fehler', content: 'Hat nicht geklappt — bitte erneut.' })
|
|
||||||
} finally {
|
|
||||||
pruefLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function nachfragen() {
|
function nachfragen() {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
import { ref } from 'vue'
|
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) {
|
export function usePruefSlot(key) {
|
||||||
if (!store.has(key)) {
|
if (!store.has(key)) {
|
||||||
@@ -17,6 +17,8 @@ export function usePruefSlot(key) {
|
|||||||
letztesFeedback: ref(''),
|
letztesFeedback: ref(''),
|
||||||
scoreVorFrage: ref(0),
|
scoreVorFrage: ref(0),
|
||||||
streakVorFrage: 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)
|
return store.get(key)
|
||||||
|
|||||||
Reference in New Issue
Block a user