update
This commit is contained in:
@@ -78,12 +78,12 @@ export async function chatBaustein({ topic, baustein, section, section_kompakt =
|
||||
export async function pruefeBaustein({
|
||||
topic, baustein, section, section_kompakt = '', provider,
|
||||
aktion = 'frage', frage = '', letzte_bewertung = '', vermeide = [],
|
||||
tier2 = false, tier3 = false, messages = [], gruendlich = false,
|
||||
nachgefragt = false, begruendung = '', tier2 = false, tier3 = false, messages = [], gruendlich = false,
|
||||
}) {
|
||||
const res = await fetch(`${BASE}/bausteine/pruefung`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ topic, baustein, section, section_kompakt, aktion, frage, letzte_bewertung, vermeide, tier2, tier3, messages, provider, gruendlich }),
|
||||
body: JSON.stringify({ topic, baustein, section, section_kompakt, aktion, frage, letzte_bewertung, vermeide, nachgefragt, begruendung, tier2, tier3, messages, provider, gruendlich }),
|
||||
})
|
||||
return jsonOrThrow(res)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user