|
|
|
|
@@ -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 {
|
|
|
|
|
|