This commit is contained in:
team3
2026-06-12 17:18:42 +02:00
parent cfc666055c
commit 78d5833fe4
38 changed files with 1854 additions and 740 deletions

View File

@@ -0,0 +1,277 @@
<script setup>
import { computed, nextTick, ref } from 'vue'
import { chatBaustein, createVertiefung, fetchVertiefung, pruefeBaustein } from '../api.js'
import { renderMarkdown } from '../markdown.js'
import { useChat } from '../composables/useChat.js'
const props = defineProps({
topic: { type: String, required: true },
baustein: { type: String, required: true },
section: { type: String, default: '' },
provider: { type: String, default: 'claude' },
status: { type: Object, default: null }, // {gute_antworten, absolviert, vertiefung}
})
const emit = defineEmits(['statusChanged'])
const NOETIG = 3
const st = computed(() => props.status || { gute_antworten: 0, absolviert: false, vertiefung: false })
// --- Toggle-Bereich ---
const activeTab = ref(null) // null | 'vertiefung' | 'chat' | 'pruefung'
function toggle(tab) {
activeTab.value = activeTab.value === tab ? null : tab
if (activeTab.value === 'vertiefung') openVertiefung()
if (activeTab.value === 'pruefung') startPruefung()
}
// --- Vertiefung (persistiert) ---
const vert = ref(null)
const vertLoading = ref(false)
const vertError = ref('')
async function openVertiefung() {
if (vert.value !== null || vertLoading.value || !st.value.vertiefung) return
vertLoading.value = true
vertError.value = ''
try {
vert.value = (await fetchVertiefung(props.topic, props.baustein)).md
} catch (e) {
vertError.value = e.message
} finally {
vertLoading.value = false
}
}
async function generateVertiefung() {
vertLoading.value = true
vertError.value = ''
try {
vert.value = (await createVertiefung({
topic: props.topic, baustein: props.baustein, section: props.section, provider: props.provider,
})).md
emit('statusChanged', { ...st.value, vertiefung: true })
} catch (e) {
vertError.value = e.message
} finally {
vertLoading.value = false
}
}
// --- Bausteinchat (flüchtig) ---
const chat = useChat((msgs) => chatBaustein({
topic: props.topic, baustein: props.baustein, section: props.section,
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)
function applyPruefung(res) {
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
try {
const res = await pruefeBaustein({
topic: props.topic, baustein: props.baustein, section: props.section,
messages: [], provider: props.provider,
})
pruefung.messages.value.push({ role: 'assistant', content: res.reply })
applyPruefung(res)
nextTick(() => pruefung.inputEl.value?.focus())
} catch {
pruefung.messages.value.push({ role: 'assistant', content: 'Fehler beim Start der Prüfung — Tab erneut öffnen.' })
} finally {
startLoading.value = false
}
}
</script>
<template>
<div class="bp">
<div class="bp-toggles">
<button :class="{ active: activeTab === 'vertiefung' }" @click="toggle('vertiefung')">
Vertiefung
</button>
<button :class="{ active: activeTab === 'chat' }" @click="toggle('chat')">
Chat
</button>
<button :class="{ active: activeTab === 'pruefung' }" @click="toggle('pruefung')">
Prüfung
<span v-if="st.absolviert" class="bp-chip done"></span>
<span v-else-if="st.gute_antworten" class="bp-chip">{{ Math.min(st.gute_antworten, NOETIG) }}/{{ NOETIG }}</span>
</button>
</div>
<div v-if="activeTab" class="bp-panel">
<!-- Vertiefung -->
<div v-if="activeTab === 'vertiefung'">
<p v-if="vertLoading" class="bp-hint">{{ vert === null ? 'Generiere Vertiefung' : 'Lade' }}</p>
<template v-else-if="vert">
<div class="markdown" v-html="renderMarkdown(vert)"></div>
<button class="bp-action" @click="generateVertiefung">Neu generieren</button>
</template>
<template v-else>
<p class="bp-hint">Noch keine Vertiefung zu diesem Baustein.</p>
<button class="bp-action" @click="generateVertiefung">Vertiefung generieren</button>
</template>
<p v-if="vertError" class="bp-error">{{ vertError }}</p>
</div>
<!-- Bausteinchat -->
<div v-else-if="activeTab === 'chat'">
<div :ref="chat.messagesEl" class="bp-messages">
<p v-if="!chat.messages.value.length" class="bp-hint">Frag etwas zu diesem Baustein. Der Verlauf wird nicht gespeichert.</p>
<template v-for="(m, i) in chat.messages.value" :key="i">
<div v-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="chat.loading.value" class="bp-msg assistant bp-typing">Denkt</div>
</div>
<div class="bp-input">
<textarea
:ref="chat.inputEl"
v-model="chat.input.value"
rows="2"
placeholder="Frage zum Baustein…"
@keydown.enter.exact.prevent="chat.send"
></textarea>
<button :disabled="!chat.input.value.trim() && !chat.loading.value" :class="{ cancel: chat.loading.value }" @click="chat.send">
{{ chat.loading.value ? '' : '' }}
</button>
</div>
</div>
<!-- Prüfung -->
<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>
</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">
<div v-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>
<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>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.bp { margin-top: 0.75rem; }
.bp-toggles { display: flex; gap: 0.4rem; }
.bp-toggles button {
display: inline-flex; align-items: center; gap: 0.35rem;
padding: 0.25rem 0.7rem;
font-size: 0.8rem;
border: 1px solid var(--border);
border-radius: 999px;
background: var(--panel-soft);
color: var(--text-muted);
cursor: pointer;
}
.bp-toggles button:hover { border-color: var(--border-strong); color: var(--text); }
.bp-toggles button.active { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.bp-chip {
font-size: 0.7rem;
padding: 0 0.35rem;
border-radius: 999px;
background: var(--panel);
border: 1px solid var(--border);
color: var(--text-muted);
}
.bp-chip.done { background: var(--success-soft); border-color: var(--success-border); color: var(--success); }
.bp-panel {
margin-top: 0.6rem;
padding: 0.75rem 0.9rem;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--panel-soft);
}
.bp-hint { font-size: 0.85rem; color: var(--text-muted); margin: 0 0 0.5rem; }
.bp-error { font-size: 0.85rem; color: var(--danger); margin: 0.5rem 0 0; }
.bp-action {
margin-top: 0.5rem;
padding: 0.3rem 0.8rem;
font-size: 0.8rem;
border: 1px solid var(--border-strong);
border-radius: 6px;
background: var(--panel);
color: var(--text);
cursor: pointer;
}
.bp-action:hover { border-color: var(--accent); }
.bp-messages { display: flex; flex-direction: column; gap: 0.4rem; max-height: 320px; overflow-y: auto; }
.bp-msg {
max-width: 88%;
padding: 0.4rem 0.65rem;
border-radius: 10px;
font-size: 0.88rem;
line-height: 1.45;
overflow-wrap: anywhere;
}
.bp-msg.user { align-self: flex-end; background: var(--accent); color: var(--on-accent); white-space: pre-wrap; }
.bp-msg.assistant { align-self: flex-start; background: var(--panel); border: 1px solid var(--border); }
.bp-typing { color: var(--text-faint); font-style: italic; }
.bp-input { display: flex; gap: 0.4rem; margin-top: 0.55rem; align-items: flex-end; }
.bp-input textarea {
flex: 1;
resize: none;
padding: 0.45rem 0.6rem;
font: inherit;
font-size: 0.88rem;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--panel);
color: var(--text);
}
.bp-input button {
padding: 0.45rem 0.7rem;
border: none;
border-radius: 8px;
background: var(--accent);
color: var(--on-accent);
cursor: pointer;
}
.bp-input button:disabled { opacity: 0.5; cursor: default; }
.bp-input button.cancel { background: var(--danger); }
</style>