This commit is contained in:
team3
2026-06-19 14:04:19 +02:00
parent 26e097dc5a
commit 0868a768a2
7 changed files with 219 additions and 157 deletions

View File

@@ -56,6 +56,9 @@ const letztesFeedback = slot.letztesFeedback // Kontext für die Diskussion üb
// Transient (pro Instanz): Eingabe-Entwurf, Lade-Spinner.
const pruefInput = ref('')
const pruefLoading = ref(false)
const nachgefragt = ref(false) // für die laufende Frage wurde nachgefragt → Gewinn max +1
const gruendlichMsg = ref(null) // Feedback-Bubble, für die das Gründlich-Feld offen ist
const gruendlichText = ref('') // optionale Begründung für „Gründlich prüfen"
const pruefMessagesEl = ref(null)
const pruefInputEl = ref(null)
@@ -109,6 +112,8 @@ async function pruefSenden(payload, onOk) {
function frageZeigen(text) {
aktuelleFrage.value = text
letztesFeedback.value = ''
nachgefragt.value = false
gruendlichMsg.value = null
pruefMessages.value.push({ role: 'assistant', kind: 'frage', content: text })
pruefPhase.value = 'frage_offen'
}
@@ -206,6 +211,7 @@ async function naechsteFrageZeigen() {
function nachfragen() {
const text = pruefInput.value.trim()
if (!text || pruefLoading.value) return
nachgefragt.value = true // Hilfe genutzt → Gewinn für diese Frage max +1
pruefMessages.value.push({ role: 'user', kind: 'nachfrage', content: text })
pruefInput.value = ''
pruefSenden(
@@ -214,13 +220,15 @@ function nachfragen() {
)
}
let letzteFeedbackMsg = null // Referenz auf die zuletzt gezeigte Bewertungs-Bubble
let pruefenRun = 0 // nur die jüngste Bewertung darf die Anzeige setzen
let letzteFeedbackMsg = null // zuletzt gezeigte Bewertungs-Bubble
let pruefenRun = 0 // nur die jüngste Schnell-Bewertung darf anzeigen
// Antwort verbindlich prüfen — der Prüfer (Bewerter + Kritiker) vergibt die Punkte.
// Während des Wartens zeigt das „…"-Bubble den Geprüft-Zustand. gruendlich=true:
// starkes Modell, re-bewertet dieselbe Frage und aktualisiert die bestehende Bubble.
async function bewertenLassen(gruendlich = false) {
function bewertungPayload() {
return { frage: aktuelleFrage.value, tier2: props.tier2, tier3: props.tier3, nachgefragt: nachgefragt.value }
}
// Agent 1 (schnell): sofortiges Niveau + Punkte zeigen, dann Agent 2 (genau) im Hintergrund.
async function schnellBewerten() {
const meine = ++pruefenRun
pruefStick.value = true
pruefLoading.value = true
@@ -228,22 +236,17 @@ async function bewertenLassen(gruendlich = false) {
try {
const res = await pruefeBaustein({
topic: props.topic, baustein: props.baustein, section: props.section, section_kompakt: props.sectionKompakt,
provider: props.provider, messages: pruefDialog(),
aktion: 'antwort_pruefen', frage: aktuelleFrage.value, tier2: props.tier2, tier3: props.tier3, gruendlich,
provider: props.provider, messages: pruefDialog(), aktion: 'antwort', ...bewertungPayload(),
})
if (meine !== pruefenRun) return
applyPruefung(res)
letztesFeedback.value = res.feedback || ''
if (gruendlich && letzteFeedbackMsg) {
letzteFeedbackMsg.content = res.feedback || letzteFeedbackMsg.content
letzteFeedbackMsg.bewertung = res.bewertung
} else {
pruefMessages.value.push({ role: 'assistant', kind: 'feedback', content: res.feedback || '', bewertung: res.bewertung, geprueft: true })
letzteFeedbackMsg = pruefMessages.value[pruefMessages.value.length - 1]
pruefPhase.value = 'bewertet'
}
pruefMessages.value.push({ role: 'assistant', kind: 'feedback', content: res.feedback || '', bewertung: res.bewertung, niveau: res.niveau, geprueft: false })
letzteFeedbackMsg = pruefMessages.value[pruefMessages.value.length - 1]
pruefPhase.value = 'bewertet'
pruefScroll()
nextTick(() => pruefInputEl.value?.focus())
genauBewerten() // Agent 2 korrigiert im Hintergrund
} catch {
if (meine === pruefenRun) pruefMessages.value.push({ role: 'assistant', kind: 'fehler', content: 'Hat nicht geklappt — bitte erneut.' })
} finally {
@@ -251,12 +254,36 @@ async function bewertenLassen(gruendlich = false) {
}
}
// Agent 2 (genau): Bewerter + Kritiker. Korrigiert Bubble + Score (Server-Wahrheit, immer übernehmen).
// gruendlich=true: starkes Modell, optional mit Begründung des Lerners.
async function genauBewerten(gruendlich = false, begruendung = '') {
const ziel = letzteFeedbackMsg
if (gruendlich) pruefLoading.value = true
try {
const res = await pruefeBaustein({
topic: props.topic, baustein: props.baustein, section: props.section, section_kompakt: props.sectionKompakt,
provider: props.provider, messages: pruefDialog(), aktion: 'antwort_pruefen', ...bewertungPayload(), gruendlich, begruendung,
})
applyPruefung(res)
if (ziel) {
ziel.content = res.feedback || ziel.content
ziel.bewertung = res.bewertung
ziel.niveau = res.niveau
ziel.geprueft = true
}
} catch {
if (ziel) ziel.geprueft = true
} finally {
if (gruendlich) pruefLoading.value = false
}
}
function antwortAbgeben() {
const text = pruefInput.value.trim()
if (!text || pruefLoading.value) return
pruefMessages.value.push({ role: 'user', kind: 'antwort', content: text })
pruefInput.value = ''
bewertenLassen(false)
schnellBewerten()
}
// Lief schon eine Diskussion? Dann darf der Verlauf direkt bewertet werden.
@@ -265,17 +292,25 @@ const hatDiskussion = computed(() => pruefMessages.value.some((m) => m.kind ===
// „Verlauf bewerten": den bisherigen Dialog als Antwort werten (Feld wird ignoriert).
function verlaufBewerten() {
if (pruefLoading.value) return
bewertenLassen(false)
schnellBewerten()
}
// „Gründlich prüfen": dieselbe Antwort vom starken Modell (role guide) neu bewerten.
function gruendlichPruefen() {
// Rechtsklick auf eine Bewertung → Gründlich-Feld öffnen; Absenden re-bewertet (mit Begründung).
function gruendlichOeffnen(msg) {
gruendlichText.value = ''
gruendlichMsg.value = msg
nextTick(() => document.getElementById('bp-gruendlich-input')?.focus())
}
function gruendlichAbsenden() {
if (pruefLoading.value) return
bewertenLassen(true)
letzteFeedbackMsg = gruendlichMsg.value || letzteFeedbackMsg
const text = gruendlichText.value.trim()
gruendlichMsg.value = null
genauBewerten(true, text)
}
// Abbruch wie im Chat: Run-Counter hochzählen → laufendes Ergebnis (pruefSenden
// und bewertenLassen) wird verworfen, Buttons sofort frei. Agent läuft serverseitig aus.
// und schnellBewerten) wird verworfen, Buttons sofort frei. Agent läuft serverseitig aus.
function pruefAbbrechen() {
if (!pruefLoading.value) return
pruefRun++
@@ -298,10 +333,9 @@ onUnmounted(() => {
window.removeEventListener('keydown', onPruefKey)
})
// ALT+1 Nachfragen/Nachhaken · ALT+2 primärer Button je Phase (idle: Frage anfordern,
// frage_offen: Antwort abgeben, bewertet: Nächste Frage) · ALT+3 Verlauf bewerten / Gründlich.
// window-Listener (nicht div), damit es auch ohne Fokus im Feld greift + den Browser-Default
// (Tab-Wechsel) unterdrückt. Nur im Prüfungs-Tab.
// ALT+1 Nachfragen (nur frage_offen) · ALT+2 primärer Button je Phase (idle: Frage anfordern,
// frage_offen: Antwort abgeben, bewertet: Nächste Frage) · ALT+3 Verlauf bewerten (frage_offen).
// Gründlich prüfen läuft per Rechtsklick auf die Bewertung, nicht über ALT.
function onPruefKey(e) {
if (!e.altKey || e.repeat) return
if (props.mode !== 'full' || activeTab.value !== 'pruefung') return
@@ -309,14 +343,13 @@ function onPruefKey(e) {
e.preventDefault()
if (pruefLoading.value) return
if (e.key === '1') {
if (pruefPhase.value !== 'idle' && pruefInput.value.trim()) nachfragen()
if (pruefPhase.value === 'frage_offen' && pruefInput.value.trim()) nachfragen()
} else if (e.key === '2') {
if (pruefPhase.value === 'idle') frageAnfordern()
else if (pruefPhase.value === 'frage_offen') { if (pruefInput.value.trim()) antwortAbgeben() }
else naechsteFrageZeigen()
} else if (e.key === '3') {
if (pruefPhase.value === 'bewertet') gruendlichPruefen()
else if (pruefPhase.value === 'frage_offen' && hatDiskussion.value) verlaufBewerten()
if (pruefPhase.value === 'frage_offen' && hatDiskussion.value) verlaufBewerten()
}
}
</script>
@@ -372,16 +405,23 @@ function onPruefKey(e) {
<div v-else>
<p class="bp-hint">
<template v-if="st.gemeistert"> Gemeistert ({{ MEISTERN }}/{{ MEISTERN }}) Max. Du kannst dich weiter prüfen, ohne Punkte.</template>
<template v-else-if="st.verstanden && tier3">Meisterpfad: {{ st.gute_antworten }}/{{ MEISTERN }}. Richtig = +1, falsch = 2 (nicht unter {{ MAX }}). Bei {{ MEISTERN }} gemeistert.</template>
<template v-else-if="st.verstanden && tier3">Meisterpfad: {{ st.gute_antworten }}/{{ MEISTERN }}. Je besser, desto mehr (+1/+2/+3), schwach = 1 (nicht unter {{ MAX }}). Bei {{ MEISTERN }} gemeistert.</template>
<template v-else-if="st.verstanden"> Verstanden. Der Meisterpfad ({{ MAX }}{{ MEISTERN }}) öffnet, sobald der ganze Guide verstanden ist.</template>
<template v-else-if="st.absolviert && tier2">Mastery: {{ st.gute_antworten }}/{{ MAX }}. Richtig = +1, falsch = 1 (nicht unter {{ NOETIG }}). Bei {{ MAX }} verstanden.</template>
<template v-else-if="st.absolviert && tier2">Mastery: {{ st.gute_antworten }}/{{ MAX }}. Je besser, desto mehr (+1/+2/+3), schwach = 1 (nicht unter {{ NOETIG }}). Bei {{ MAX }} verstanden.</template>
<template v-else-if="st.absolviert"> Absolviert. Mehr ({{ NOETIG }}{{ MAX }}) gibt's, sobald der ganze Guide absolviert ist.</template>
<template v-else>{{ Math.min(st.gute_antworten, NOETIG) }}/{{ NOETIG }} guten Antworten. Frag nach, wenn etwas unklar ist diskutieren ist erlaubt.</template>
<template v-else>{{ Math.min(st.gute_antworten, NOETIG) }}/{{ NOETIG }} guten Antworten. Je besser deine Antwort, desto mehr Punkte (bis +3). Nachfragen ist erlaubt dann max +1.</template>
</p>
<div v-if="pruefMessages.length" ref="pruefMessagesEl" class="bp-messages" @scroll="onPruefScroll">
<template v-for="(m, i) in pruefMessages" :key="i">
<div v-if="m.kind === 'feedback'" class="bp-feedback" :class="m.bewertung">{{ m.content }}<span v-if="!m.geprueft" class="bp-pruefend"> · wird geprüft</span></div>
<div v-if="m.kind === 'feedback'" class="bp-feedback" :class="m.bewertung" title="Rechtsklick: gründlich prüfen" @contextmenu.prevent="gruendlichOeffnen(m)">
<span v-if="m.niveau" class="bp-niveau">{{ m.niveau }}</span>{{ m.content }}<span v-if="!m.geprueft" class="bp-pruefend"> · wird geprüft</span>
<div v-if="gruendlichMsg === m" class="bp-gruendlich" @click.stop>
<input id="bp-gruendlich-input" v-model="gruendlichText" placeholder="Warum unzufrieden? (optional)" @keyup.enter="gruendlichAbsenden" />
<button class="bp-action primary" @click="gruendlichAbsenden">Gründlich prüfen</button>
<button class="bp-action" @click="gruendlichMsg = null">×</button>
</div>
</div>
<div v-else-if="m.kind === 'fehler'" class="bp-error">{{ m.content }}</div>
<div v-else-if="m.role === 'assistant'" class="bp-msg assistant markdown" v-html="renderMarkdown(m.content)"></div>
<div v-else class="bp-msg user">{{ m.content }}</div>
@@ -395,14 +435,14 @@ function onPruefKey(e) {
<button v-else class="bp-action primary" @click="frageAnfordern">Frage anfordern</button>
</div>
<!-- Phase frage_offen / bewertet: Textfeld + Aktionen -->
<!-- Phase frage_offen: Textfeld + Aktionen · bewertet: nur Nächste Frage" -->
<template v-else>
<div class="bp-input">
<div v-if="pruefPhase === 'frage_offen'" class="bp-input">
<textarea
ref="pruefInputEl"
v-model="pruefInput"
rows="2"
:placeholder="pruefPhase === 'frage_offen' ? 'Antwort — oder Nachfrage bei Unklarheit…' : 'Nachhaken oder diskutieren…'"
placeholder="Antwort — oder Nachfrage bei Unklarheit…"
></textarea>
</div>
<div class="bp-actions">
@@ -413,9 +453,7 @@ function onPruefKey(e) {
<button v-if="hatDiskussion" class="bp-action" title="Alt+3" @click="verlaufBewerten"><span class="bp-kbd">3</span>Verlauf bewerten</button>
</template>
<template v-else>
<button class="bp-action" title="Alt+1" :disabled="!pruefInput.trim()" @click="nachfragen"><span class="bp-kbd">1</span>Nachhaken</button>
<button class="bp-action primary" title="Alt+2" @click="naechsteFrageZeigen"><span class="bp-kbd">2</span>Nächste Frage</button>
<button class="bp-action" title="Alt+3" @click="gruendlichPruefen"><span class="bp-kbd">3</span>Gründlich prüfen</button>
</template>
</div>
</template>
@@ -530,8 +568,18 @@ function onPruefKey(e) {
border: 1px solid var(--border);
}
.bp-pruefend { font-style: italic; opacity: 0.7; font-size: 0.92em; }
.bp-feedback { cursor: context-menu; }
.bp-feedback.gut { background: var(--success-soft); border-color: var(--success-border); color: var(--success); }
.bp-feedback.neutral { background: var(--warning-soft); border-color: var(--warning-border); color: var(--warning); }
.bp-feedback.schlecht { background: var(--danger-soft, #fee2e2); border-color: var(--danger-border, #f87171); color: var(--danger); }
.bp-niveau { font-weight: 700; text-transform: uppercase; font-size: 0.62rem; letter-spacing: 0.03em; margin-right: 0.4rem; opacity: 0.85; }
/* Gründlich-prüfen-Feld (per Rechtsklick) innerhalb der Bewertungs-Bubble */
.bp-gruendlich { display: flex; gap: 0.3rem; margin-top: 0.4rem; align-items: center; }
.bp-gruendlich input {
flex: 1; min-width: 0; padding: 0.3rem 0.5rem; font: inherit; font-size: 0.8rem;
border: 1px solid var(--border-strong); border-radius: 6px; background: var(--panel); color: var(--text);
}
.bp-gruendlich .bp-action { margin-top: 0; }
.bp-input { display: flex; gap: 0.4rem; margin-top: 0.55rem; align-items: flex-end; }
.bp-input textarea {