update
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { chatBaustein, createVertiefung, fetchVertiefung, pruefeBaustein } from '../api.js'
|
||||
import { chatBaustein, pruefeBaustein } from '../api.js'
|
||||
import { usePruefSlot } from '../pruefungCache.js'
|
||||
import { renderMarkdown } from '../markdown.js'
|
||||
import { useChat, istUnten } from '../composables/useChat.js'
|
||||
@@ -8,77 +8,38 @@ import { useChat, istUnten } from '../composables/useChat.js'
|
||||
const props = defineProps({
|
||||
topic: { type: String, required: true },
|
||||
baustein: { type: String, required: true },
|
||||
section: { type: String, default: '' },
|
||||
section: { type: String, default: '' }, // ausführliche Fassung
|
||||
sectionKompakt: { type: String, default: '' }, // kompakte Fassung (Merksätze) — Prüfungs-/Chat-Kontext
|
||||
provider: { type: String, default: 'claude' },
|
||||
status: { type: Object, default: null }, // {gute_antworten, absolviert, verstanden, gemeistert, vertiefung}
|
||||
status: { type: Object, default: null }, // {gute_antworten, streak, absolviert, verstanden, gemeistert}
|
||||
tier2: { type: Boolean, default: false }, // Tier 2 frei (ganzer Guide absolviert)
|
||||
tier3: { type: Boolean, default: false }, // Meisterpfad frei (ganzer Guide verstanden)
|
||||
// 'trigger' (Liste): nur Tab-Leiste, Klick öffnet den Vollbild-Fokus.
|
||||
// 'full' (Fokus): Tabs + Panel-Inhalt wie gehabt.
|
||||
mode: { type: String, default: 'trigger' },
|
||||
initialTab: { type: String, default: null },
|
||||
ansicht: { type: String, default: 'kompakt' }, // kompakt | erklärend — steuert die linke Guide-Spalte
|
||||
})
|
||||
|
||||
const emit = defineEmits(['statusChanged', 'openFokus'])
|
||||
const emit = defineEmits(['statusChanged', 'openFokus', 'setAnsicht'])
|
||||
|
||||
const NOETIG = 3 // absolviert
|
||||
const MAX = 10 // verstanden
|
||||
const MEISTERN = 25 // gemeistert (Maximum)
|
||||
const st = computed(() => props.status || { gute_antworten: 0, streak: 0, absolviert: false, verstanden: false, gemeistert: false, vertiefung: false, deepdive: false })
|
||||
const st = computed(() => props.status || { gute_antworten: 0, streak: 0, absolviert: false, verstanden: false, gemeistert: false })
|
||||
|
||||
// --- Toggle-Bereich ---
|
||||
const activeTab = ref(props.mode === 'full' ? props.initialTab : null) // null | 'vertiefung' | 'deepdive' | 'chat' | 'pruefung'
|
||||
const activeTab = ref(props.mode === 'full' ? props.initialTab : null) // null | 'chat' | 'pruefung'
|
||||
|
||||
// Klick auf einen Tab: in der Liste den Fokus öffnen, im Fokus den Tab umschalten.
|
||||
function tabKlick(tab) {
|
||||
if (props.mode === 'trigger') { emit('openFokus', tab); return }
|
||||
toggle(tab)
|
||||
}
|
||||
|
||||
function toggle(tab) {
|
||||
activeTab.value = activeTab.value === tab ? null : tab
|
||||
if (activeTab.value === 'vertiefung' || activeTab.value === 'deepdive') openText(activeTab.value)
|
||||
}
|
||||
|
||||
// --- Vertiefung (gleicher Stoff, mehr) + Amateur (gleicher Stoff, für Einsteiger), beide persistiert (intern art 'deepdive') ---
|
||||
const texte = ref({
|
||||
vertiefung: { md: null, loading: false, error: '' },
|
||||
deepdive: { md: null, loading: false, error: '' },
|
||||
})
|
||||
|
||||
async function openText(art) {
|
||||
const t = texte.value[art]
|
||||
if (t.md !== null || t.loading || !st.value[art]) return
|
||||
t.loading = true
|
||||
t.error = ''
|
||||
try {
|
||||
t.md = (await fetchVertiefung(props.topic, props.baustein, art)).md
|
||||
} catch (e) {
|
||||
t.error = e.message
|
||||
} finally {
|
||||
t.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function generateText(art) {
|
||||
const t = texte.value[art]
|
||||
t.loading = true
|
||||
t.error = ''
|
||||
try {
|
||||
t.md = (await createVertiefung({
|
||||
topic: props.topic, baustein: props.baustein, section: props.section, art, provider: props.provider,
|
||||
})).md
|
||||
emit('statusChanged', { ...st.value, [art]: true })
|
||||
} catch (e) {
|
||||
t.error = e.message
|
||||
} finally {
|
||||
t.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
// --- Bausteinchat (flüchtig) ---
|
||||
const chat = useChat((msgs) => chatBaustein({
|
||||
topic: props.topic, baustein: props.baustein, section: props.section,
|
||||
topic: props.topic, baustein: props.baustein, section: props.section, section_kompakt: props.sectionKompakt,
|
||||
messages: msgs, provider: props.provider,
|
||||
}))
|
||||
|
||||
@@ -131,7 +92,7 @@ async function pruefSenden(payload, onOk) {
|
||||
pruefScroll()
|
||||
try {
|
||||
const res = await pruefeBaustein({
|
||||
topic: props.topic, baustein: props.baustein, section: props.section,
|
||||
topic: props.topic, baustein: props.baustein, section: props.section, section_kompakt: props.sectionKompakt,
|
||||
provider: props.provider, messages: pruefDialog(), ...payload,
|
||||
})
|
||||
if (run !== pruefRun) return
|
||||
@@ -175,7 +136,7 @@ function istBekannt(text) {
|
||||
|
||||
async function frageEinzeln() {
|
||||
const res = await pruefeBaustein({
|
||||
topic: props.topic, baustein: props.baustein, section: props.section,
|
||||
topic: props.topic, baustein: props.baustein, section: props.section, section_kompakt: props.sectionKompakt,
|
||||
provider: props.provider, aktion: 'frage', messages: pruefDialog(),
|
||||
})
|
||||
return res.frage
|
||||
@@ -275,7 +236,7 @@ async function verdictPruefen(gruendlich = false) {
|
||||
if (gruendlich) pruefLoading.value = true
|
||||
try {
|
||||
const res = await pruefeBaustein({
|
||||
topic: props.topic, baustein: props.baustein, section: props.section,
|
||||
topic: props.topic, baustein: props.baustein, section: props.section, section_kompakt: props.sectionKompakt,
|
||||
provider: props.provider, messages: pruefDialog(), ...antwortPayload(), aktion: 'antwort_pruefen', gruendlich,
|
||||
})
|
||||
if (meine !== pruefenRun) return // eine neuere Bewertung läuft → diese verwerfen
|
||||
@@ -336,8 +297,6 @@ onMounted(() => {
|
||||
if (props.mode !== 'full') return // Trigger-Modus (Liste): nur Tab-Leiste, keine Prüfungs-/Text-Logik
|
||||
window.addEventListener('keydown', onWindowKey)
|
||||
window.addEventListener('keydown', onPruefKey)
|
||||
// Direkt im Fokus geöffneter Vertiefung-/Amateur-Tab: Text laden (sonst nur via toggle).
|
||||
if (activeTab.value === 'vertiefung' || activeTab.value === 'deepdive') openText(activeTab.value)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', onWindowKey)
|
||||
@@ -370,12 +329,11 @@ function onPruefKey(e) {
|
||||
<template>
|
||||
<div class="bp">
|
||||
<div class="bp-toggles">
|
||||
<button :class="{ active: activeTab === 'vertiefung' }" @click="tabKlick('vertiefung')">
|
||||
Vertiefung
|
||||
</button>
|
||||
<button :class="{ active: activeTab === 'deepdive' }" @click="tabKlick('deepdive')">
|
||||
Amateur
|
||||
</button>
|
||||
<!-- Ansicht-Umschalter (nur Vollbild): steuert die linke Guide-Spalte, links vor den Tabs -->
|
||||
<div v-if="mode === 'full'" class="bp-ansicht">
|
||||
<button :class="{ active: ansicht === 'kompakt' }" title="Merksätze" @click="emit('setAnsicht', 'kompakt')">Kompakt</button>
|
||||
<button :class="{ active: ansicht === 'erklärend' }" title="Ausführliche Erklärung" @click="emit('setAnsicht', 'erklärend')">Erklärend</button>
|
||||
</div>
|
||||
<button :class="{ active: activeTab === 'chat' }" @click="tabKlick('chat')">
|
||||
Chat
|
||||
</button>
|
||||
@@ -391,24 +349,8 @@ function onPruefKey(e) {
|
||||
</div>
|
||||
|
||||
<div v-if="mode === 'full' && activeTab" class="bp-panel">
|
||||
<!-- Vertiefung (gleicher Stoff, mehr) / Amateur (gleicher Stoff, für Einsteiger) -->
|
||||
<div v-if="activeTab === 'vertiefung' || activeTab === 'deepdive'">
|
||||
<p v-if="texte[activeTab].loading" class="bp-hint">{{ texte[activeTab].md === null ? 'Generiere…' : 'Lade…' }}</p>
|
||||
<template v-else-if="texte[activeTab].md">
|
||||
<div class="markdown" v-html="renderMarkdown(texte[activeTab].md)"></div>
|
||||
<button class="bp-action" @click="generateText(activeTab)">Neu generieren</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p class="bp-hint">{{ activeTab === 'deepdive' ? 'Noch keine Amateur-Fassung zu diesem Baustein.' : 'Noch keine Vertiefung zu diesem Baustein.' }}</p>
|
||||
<button class="bp-action" @click="generateText(activeTab)">
|
||||
{{ activeTab === 'deepdive' ? 'Amateur generieren' : 'Vertiefung generieren' }}
|
||||
</button>
|
||||
</template>
|
||||
<p v-if="texte[activeTab].error" class="bp-error">{{ texte[activeTab].error }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Bausteinchat -->
|
||||
<div v-else-if="activeTab === 'chat'">
|
||||
<div v-if="activeTab === 'chat'">
|
||||
<div :ref="chat.messagesEl" class="bp-messages" @scroll="chat.onScroll">
|
||||
<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">
|
||||
@@ -504,6 +446,21 @@ function onPruefKey(e) {
|
||||
.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); }
|
||||
|
||||
/* Ansicht-Umschalter (Kompakt/Erklärend) — segmentiert, links vor den Tabs */
|
||||
.bp-ansicht { display: inline-flex; margin-right: 0.5rem; }
|
||||
.bp-ansicht button {
|
||||
padding: 0.25rem 0.6rem;
|
||||
font-size: 0.75rem;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--panel-soft);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
.bp-ansicht button:first-child { border-radius: 999px 0 0 999px; }
|
||||
.bp-ansicht button:last-child { border-radius: 0 999px 999px 0; border-left: none; }
|
||||
.bp-ansicht button:hover { color: var(--text); }
|
||||
.bp-ansicht button.active { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
|
||||
|
||||
.bp-chip {
|
||||
font-size: 0.7rem;
|
||||
padding: 0 0.35rem;
|
||||
|
||||
Reference in New Issue
Block a user