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

@@ -1,8 +1,9 @@
<script setup>
import { computed, ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { fetchGuideContent, chatGuide, fetchProgress, setProgress } from '../api.js'
import { fetchGuideContent, chatGuide, fetchBausteinLernstand } from '../api.js'
import { renderMarkdown } from '../markdown.js'
import { useChat } from '../composables/useChat.js'
import BausteinPanel from './BausteinPanel.vue'
const props = defineProps({
previewGuide: { type: Object, default: null },
@@ -11,7 +12,7 @@ const props = defineProps({
elementsOpen: { type: Boolean, default: false }, // Element-Sidebar offen → Chat nach links
})
const emit = defineEmits(['progressChanged', 'openElements'])
const emit = defineEmits(['progressChanged'])
const isOnePager = computed(() => props.previewGuide?.format === 'OnePager')
@@ -21,7 +22,6 @@ const CH_COLORS = ['#3b82f6', '#8b5cf6', '#14b8a6', '#f59e0b', '#22c55e', '#6366
// --- Inhalt laden ---
const content = ref(null)
const loadError = ref(null)
const doneChapters = ref(new Set())
const scrollEl = ref(null)
watch(() => props.previewGuide?.id, loadContent, { immediate: true })
@@ -29,7 +29,7 @@ watch(() => props.previewGuide?.id, loadContent, { immediate: true })
async function loadContent() {
content.value = null
loadError.value = null
doneChapters.value = new Set()
lernstand.value = {}
const g = props.previewGuide
if (!g || g.status !== 'done') return
try {
@@ -39,40 +39,22 @@ async function loadContent() {
loadError.value = 'Inhalt nicht verfügbar — die Datei fehlt. Guide neu generieren (▶).'
return
}
try {
const res = await fetchProgress(g.id)
doneChapters.value = new Set(res.chapters || [])
} catch { /* offline → leer */ }
nextTick(scrollToFirstOpen)
}
// Zum ersten noch offenen Kapitel springen — aber nur, wenn schon etwas erledigt ist.
function scrollToFirstOpen() {
if (!doneChapters.value.size || !content.value) return
const chapters = Array.from(scrollEl.value?.querySelectorAll('section.chapter') || [])
const firstOpen = chapters.find((el) => !el.classList.contains('ch-complete'))
if (firstOpen && firstOpen !== chapters[0]) firstOpen.scrollIntoView({ block: 'start' })
}
// --- Kapitel-Fortschritt ---
async function toggleChapter(title) {
const newState = !doneChapters.value.has(title)
const optimistic = new Set(doneChapters.value)
if (newState) optimistic.add(title)
else optimistic.delete(title)
doneChapters.value = optimistic
try {
const res = await setProgress(props.previewGuide.id, title, newState)
doneChapters.value = new Set(res.chapters || [])
emit('progressChanged')
} catch {
const rollback = new Set(doneChapters.value)
if (newState) rollback.delete(title)
else rollback.add(title)
doneChapters.value = rollback
if (g.format !== 'OnePager') {
try {
lernstand.value = (await fetchBausteinLernstand(g.topic)).bausteine || {}
} catch { /* offline → leer */ }
}
}
// --- Baustein-Lernen: Prüfungs-Stand pro Baustein-Titel ---
const lernstand = ref({})
function onBausteinStatus(baustein, status) {
const warAbsolviert = lernstand.value[baustein]?.absolviert
lernstand.value = { ...lernstand.value, [baustein]: status }
if (status.absolviert && !warAbsolviert) emit('progressChanged') // Locks/Stats neu laden
}
// --- Chat (Mechanik in useChat; Kontext-Extraktion bleibt hier) ---
const chat = useChat((msgs) => {
const { section, outline } = extractContext()
@@ -164,7 +146,6 @@ function extractContext() {
v-for="(ch, ci) in content.chapters"
:key="ch.title"
class="chapter"
:class="{ 'ch-complete': doneChapters.has(ch.title) }"
:style="{ '--ch-accent': CH_COLORS[ci % CH_COLORS.length] }"
>
<h2 class="chapter-title"><span class="ch-num">{{ ci + 1 }}</span>{{ ch.title }}</h2>
@@ -175,16 +156,22 @@ function extractContext() {
:class="['section-card', isOnePager && s.key ? 'op-card op-' + s.key : '']"
:style="isOnePager && s.key ? { gridArea: s.key } : null"
>
<h3>{{ s.title }}</h3>
<h3>
{{ s.title }}
<span v-if="lernstand[s.title]?.absolviert" class="baustein-done" title="Baustein absolviert"></span>
</h3>
<div class="section-body markdown" v-html="renderMarkdown(s.md)"></div>
<BausteinPanel
v-if="!isOnePager"
:topic="previewGuide.topic"
:baustein="s.title"
:section="s.md"
:provider="provider"
:status="lernstand[s.title]"
@status-changed="(st) => onBausteinStatus(s.title, st)"
/>
</article>
</div>
<button
v-if="!isOnePager"
class="ch-toggle"
:class="{ 'is-done': doneChapters.has(ch.title) }"
@click="toggleChapter(ch.title)"
>{{ doneChapters.has(ch.title) ? '✓ Erledigt rückgängig' : 'Kapitel als erledigt markieren' }}</button>
</section>
</div>
</div>
@@ -198,7 +185,6 @@ function extractContext() {
</div>
<button v-if="previewGuide && !chatOpen" class="chat-fab" :class="{ shifted: elementsOpen }" title="Fragen zum Guide" @click="openChat">💬</button>
<button v-if="previewGuide && !chatOpen && !elementsOpen" class="chat-fab elements-fab" title="Elemente öffnen" @click="emit('openElements')">🗂</button>
<div v-if="previewGuide && chatOpen" ref="panelEl" class="chat-panel" :class="{ shifted: elementsOpen }">
<header class="chat-header">
@@ -310,10 +296,6 @@ function extractContext() {
font-weight: 700;
}
.chapter.ch-complete .sections {
opacity: 0.4;
}
.section-card {
background: var(--panel);
border: 1px solid var(--border);
@@ -322,6 +304,12 @@ function extractContext() {
margin-bottom: 0.75rem;
}
.baustein-done {
margin-left: 0.35rem;
font-size: 0.85em;
color: var(--success);
}
/* Guides: Karten tragen die Kapitel-Akzentfarbe (OnePager hat eigene op-card-Farben) */
.guide-content:not(.onepager) .section-card {
border-top: 3px solid color-mix(in srgb, var(--ch-accent, var(--accent)) 65%, transparent);
@@ -438,35 +426,6 @@ function extractContext() {
}
}
.ch-toggle {
display: block;
width: 100%;
margin-top: 0.5rem;
padding: 0.8rem 1rem;
border: 1.5px dashed var(--border-strong);
border-radius: 10px;
background: var(--panel-soft);
color: var(--text-muted);
font: 600 0.9rem/1.2 inherit;
font-family: inherit;
text-align: center;
cursor: pointer;
transition: all 0.12s;
&:hover {
border-color: var(--accent);
color: var(--accent);
background: transparent;
}
&.is-done {
border-style: solid;
border-color: var(--success-border);
background: var(--success-soft);
color: var(--success);
}
}
.empty-preview {
display: flex;
align-items: center;
@@ -533,10 +492,6 @@ function extractContext() {
background: var(--accent-hover);
}
.elements-fab {
right: 5.25rem;
}
/* Element-Sidebar (320px) offen → Chat links daneben anzeigen */
.chat-fab.shifted {
right: calc(1.5rem + 320px);