update
This commit is contained in:
@@ -23,7 +23,6 @@ const activeTab = ref(null) // null | 'vertiefung' | 'deepdive' | 'chat' | 'prue
|
||||
function toggle(tab) {
|
||||
activeTab.value = activeTab.value === tab ? null : tab
|
||||
if (activeTab.value === 'vertiefung' || activeTab.value === 'deepdive') openText(activeTab.value)
|
||||
if (activeTab.value === 'pruefung') startPruefung()
|
||||
}
|
||||
|
||||
// --- Vertiefung (kurz) + Deep Dive (lang), beide persistiert ---
|
||||
@@ -68,42 +67,97 @@ const chat = useChat((msgs) => chatBaustein({
|
||||
messages: msgs, provider: props.provider,
|
||||
}))
|
||||
|
||||
// --- Prüfung (Verlauf flüchtig, Zähler serverseitig) ---
|
||||
const pruefung = useChat(async (msgs) => {
|
||||
const res = await pruefeBaustein({
|
||||
topic: props.topic, baustein: props.baustein, section: props.section,
|
||||
messages: msgs, provider: props.provider,
|
||||
})
|
||||
applyPruefung(res)
|
||||
return res
|
||||
})
|
||||
const startLoading = ref(false)
|
||||
// --- Prüfung: gesteuerter Dialog (Verlauf flüchtig, Zähler serverseitig) ---
|
||||
// Phasen: 'idle' (Frage anfordern) | 'frage_offen' (antworten/nachfragen) | 'bewertet' (diskutieren/neu bewerten/weiter)
|
||||
const pruefMessages = ref([]) // {role, kind: 'frage'|'nachfrage'|'antwort'|'feedback'|'diskussion'|'fehler', content, bewertung?}
|
||||
const pruefInput = ref('')
|
||||
const pruefPhase = ref('idle')
|
||||
const pruefLoading = ref(false)
|
||||
const aktuelleFrage = ref('') // ankert Bewertung/Diskussion
|
||||
const letztesFeedback = ref('') // Kontext für die Diskussion über eine Bewertung
|
||||
const frageSchonGut = ref(false) // diese Frage schon "gut" → nicht doppelt zählen
|
||||
const pruefMessagesEl = ref(null)
|
||||
const pruefInputEl = ref(null)
|
||||
let pruefRun = 0
|
||||
|
||||
function applyPruefung(res) {
|
||||
emit('statusChanged', {
|
||||
...st.value,
|
||||
gute_antworten: res.gute_antworten,
|
||||
absolviert: res.absolviert,
|
||||
})
|
||||
emit('statusChanged', { ...st.value, gute_antworten: res.gute_antworten, absolviert: res.absolviert })
|
||||
}
|
||||
|
||||
async function startPruefung() {
|
||||
if (pruefung.messages.value.length || startLoading.value) return
|
||||
startLoading.value = true
|
||||
async function pruefScroll() {
|
||||
await nextTick()
|
||||
if (pruefMessagesEl.value) pruefMessagesEl.value.scrollTop = pruefMessagesEl.value.scrollHeight
|
||||
}
|
||||
|
||||
// Nur echte Gesprächs-Turns ans Backend; Feedback bleibt reines UI-Artefakt.
|
||||
function pruefDialog() {
|
||||
return pruefMessages.value
|
||||
.filter((m) => m.kind !== 'feedback' && m.kind !== 'fehler')
|
||||
.map((m) => ({ role: m.role, content: m.content }))
|
||||
}
|
||||
|
||||
async function pruefSenden(payload, onOk) {
|
||||
const run = ++pruefRun
|
||||
pruefLoading.value = true
|
||||
pruefScroll()
|
||||
try {
|
||||
const res = await pruefeBaustein({
|
||||
topic: props.topic, baustein: props.baustein, section: props.section,
|
||||
messages: [], provider: props.provider,
|
||||
provider: props.provider, messages: pruefDialog(), ...payload,
|
||||
})
|
||||
pruefung.messages.value.push({ role: 'assistant', content: res.frage, feedback: null })
|
||||
if (run !== pruefRun) return
|
||||
onOk(res)
|
||||
applyPruefung(res)
|
||||
nextTick(() => pruefung.inputEl.value?.focus())
|
||||
pruefScroll()
|
||||
nextTick(() => pruefInputEl.value?.focus())
|
||||
} catch {
|
||||
pruefung.messages.value.push({ role: 'assistant', content: 'Fehler beim Start der Prüfung — Tab erneut öffnen.' })
|
||||
if (run === pruefRun) pruefMessages.value.push({ role: 'assistant', kind: 'fehler', content: 'Hat nicht geklappt — bitte erneut.' })
|
||||
} finally {
|
||||
startLoading.value = false
|
||||
if (run === pruefRun) pruefLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function frageAnfordern() {
|
||||
if (pruefLoading.value) return
|
||||
pruefSenden({ aktion: 'frage' }, (res) => {
|
||||
aktuelleFrage.value = res.frage
|
||||
letztesFeedback.value = ''
|
||||
frageSchonGut.value = false
|
||||
pruefMessages.value.push({ role: 'assistant', kind: 'frage', content: res.frage })
|
||||
pruefPhase.value = 'frage_offen'
|
||||
})
|
||||
}
|
||||
|
||||
function nachfragen() {
|
||||
const text = pruefInput.value.trim()
|
||||
if (!text || pruefLoading.value) return
|
||||
pruefMessages.value.push({ role: 'user', kind: 'nachfrage', content: text })
|
||||
pruefInput.value = ''
|
||||
pruefSenden(
|
||||
{ aktion: 'diskussion', frage: aktuelleFrage.value, letzte_bewertung: letztesFeedback.value },
|
||||
(res) => pruefMessages.value.push({ role: 'assistant', kind: 'diskussion', content: res.reply }),
|
||||
)
|
||||
}
|
||||
|
||||
function bewerten(res) {
|
||||
letztesFeedback.value = res.feedback || ''
|
||||
if (res.bewertung === 'gut') frageSchonGut.value = true
|
||||
pruefMessages.value.push({ role: 'assistant', kind: 'feedback', content: res.feedback || '', bewertung: res.bewertung })
|
||||
pruefPhase.value = 'bewertet'
|
||||
}
|
||||
|
||||
function antwortAbgeben() {
|
||||
const text = pruefInput.value.trim()
|
||||
if (!text || pruefLoading.value) return
|
||||
pruefMessages.value.push({ role: 'user', kind: 'antwort', content: text })
|
||||
pruefInput.value = ''
|
||||
pruefSenden({ aktion: 'antwort', frage: aktuelleFrage.value, frage_schon_gut: frageSchonGut.value }, bewerten)
|
||||
}
|
||||
|
||||
function neuBewerten() {
|
||||
if (pruefLoading.value) return
|
||||
pruefSenden({ aktion: 'antwort', frage: aktuelleFrage.value, frage_schon_gut: frageSchonGut.value }, bewerten)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -166,35 +220,50 @@ async function startPruefung() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prüfung -->
|
||||
<!-- Prüfung: gesteuerter Dialog -->
|
||||
<div v-else>
|
||||
<p class="bp-hint">
|
||||
<template v-if="st.absolviert">✓ Absolviert — du kannst dich weiter prüfen lassen.</template>
|
||||
<template v-else>{{ Math.min(st.gute_antworten, NOETIG) }}/{{ NOETIG }} guten Antworten. Erkläre in eigenen Worten — das Material darfst du nutzen.</template>
|
||||
<template v-else>{{ Math.min(st.gute_antworten, NOETIG) }}/{{ NOETIG }} guten Antworten. Frag nach, wenn etwas unklar ist — diskutieren ist erlaubt.</template>
|
||||
</p>
|
||||
<div :ref="pruefung.messagesEl" class="bp-messages">
|
||||
<div v-if="startLoading" class="bp-msg assistant bp-typing">Erste Frage kommt…</div>
|
||||
<template v-for="(m, i) in pruefung.messages.value" :key="i">
|
||||
<template v-if="m.role === 'assistant'">
|
||||
<div v-if="m.feedback" class="bp-feedback" :class="m.bewertung">{{ m.feedback }}</div>
|
||||
<div class="bp-msg assistant markdown" v-html="renderMarkdown(m.content)"></div>
|
||||
</template>
|
||||
|
||||
<div v-if="pruefMessages.length" :ref="pruefMessagesEl" class="bp-messages">
|
||||
<template v-for="(m, i) in pruefMessages" :key="i">
|
||||
<div v-if="m.kind === 'feedback'" class="bp-feedback" :class="m.bewertung">{{ m.content }}</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>
|
||||
</template>
|
||||
<div v-if="pruefung.loading.value" class="bp-msg assistant bp-typing">Bewertet…</div>
|
||||
<div v-if="pruefLoading" class="bp-msg assistant bp-typing">…</div>
|
||||
</div>
|
||||
<div class="bp-input">
|
||||
<textarea
|
||||
:ref="pruefung.inputEl"
|
||||
v-model="pruefung.input.value"
|
||||
rows="2"
|
||||
placeholder="Deine Erklärung…"
|
||||
@keydown.enter.exact.prevent="pruefung.send"
|
||||
></textarea>
|
||||
<button :disabled="!pruefung.input.value.trim() && !pruefung.loading.value" :class="{ cancel: pruefung.loading.value }" @click="pruefung.send">
|
||||
{{ pruefung.loading.value ? '✕' : '➤' }}
|
||||
</button>
|
||||
|
||||
<!-- Phase idle: Frage anfordern -->
|
||||
<div v-if="pruefPhase === 'idle'" class="bp-actions">
|
||||
<button class="bp-action primary" :disabled="pruefLoading" @click="frageAnfordern">Frage anfordern</button>
|
||||
</div>
|
||||
|
||||
<!-- Phase frage_offen / bewertet: Textfeld + Aktionen -->
|
||||
<template v-else>
|
||||
<div class="bp-input">
|
||||
<textarea
|
||||
:ref="pruefInputEl"
|
||||
v-model="pruefInput"
|
||||
rows="2"
|
||||
:placeholder="pruefPhase === 'frage_offen' ? 'Antwort — oder Nachfrage bei Unklarheit…' : 'Nachhaken oder diskutieren…'"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="bp-actions">
|
||||
<template v-if="pruefPhase === 'frage_offen'">
|
||||
<button class="bp-action" :disabled="pruefLoading || !pruefInput.trim()" @click="nachfragen">Nachfragen</button>
|
||||
<button class="bp-action primary" :disabled="pruefLoading || !pruefInput.trim()" @click="antwortAbgeben">Antwort abgeben</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button class="bp-action" :disabled="pruefLoading || !pruefInput.trim()" @click="nachfragen">Nachhaken</button>
|
||||
<button class="bp-action" :disabled="pruefLoading" @click="neuBewerten">Neu bewerten</button>
|
||||
<button class="bp-action primary" :disabled="pruefLoading" @click="frageAnfordern">Nächste Frage</button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -249,6 +318,12 @@ async function startPruefung() {
|
||||
cursor: pointer;
|
||||
}
|
||||
.bp-action:hover { border-color: var(--accent); }
|
||||
.bp-action:disabled { opacity: 0.5; cursor: default; }
|
||||
.bp-action.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
|
||||
.bp-action.primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
||||
|
||||
.bp-actions { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.5rem; }
|
||||
.bp-actions .bp-action { margin-top: 0; }
|
||||
|
||||
.bp-messages { display: flex; flex-direction: column; gap: 0.4rem; max-height: 320px; overflow-y: auto; }
|
||||
.bp-msg {
|
||||
|
||||
Reference in New Issue
Block a user