update
This commit is contained in:
@@ -393,7 +393,6 @@ onMounted(async () => {
|
||||
:provider="provider"
|
||||
:elementsOpen="elementsOpen"
|
||||
@progressChanged="loadStats(); loadBausteine()"
|
||||
@openElements="elementsOpen = true"
|
||||
/>
|
||||
<div v-else class="empty-main">
|
||||
<p>Thema in der Sidebar anlegen oder auswählen.</p>
|
||||
|
||||
@@ -59,6 +59,47 @@ export async function deleteBausteine(topic) {
|
||||
await fetch(`${BASE}/bausteine?topic=${encodeURIComponent(topic)}`, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
// --- Baustein-Lernen: Vertiefung, Chat, Prüfung ---
|
||||
|
||||
export async function fetchBausteinLernstand(topic) {
|
||||
const res = await fetch(`${BASE}/bausteine/lernstand?topic=${encodeURIComponent(topic)}`)
|
||||
return jsonOrThrow(res)
|
||||
}
|
||||
|
||||
export async function fetchVertiefung(topic, baustein) {
|
||||
const res = await fetch(
|
||||
`${BASE}/bausteine/vertiefung?topic=${encodeURIComponent(topic)}&baustein=${encodeURIComponent(baustein)}`
|
||||
)
|
||||
return jsonOrThrow(res)
|
||||
}
|
||||
|
||||
export async function createVertiefung({ topic, baustein, section, provider }) {
|
||||
const res = await fetch(`${BASE}/bausteine/vertiefung`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ topic, baustein, section, provider }),
|
||||
})
|
||||
return jsonOrThrow(res)
|
||||
}
|
||||
|
||||
export async function chatBaustein({ topic, baustein, section, messages, provider }) {
|
||||
const res = await fetch(`${BASE}/bausteine/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ topic, baustein, section, messages, provider }),
|
||||
})
|
||||
return jsonOrThrow(res)
|
||||
}
|
||||
|
||||
export async function pruefeBaustein({ topic, baustein, section, messages, provider }) {
|
||||
const res = await fetch(`${BASE}/bausteine/pruefung`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ topic, baustein, section, messages, provider }),
|
||||
})
|
||||
return jsonOrThrow(res)
|
||||
}
|
||||
|
||||
export async function fetchTopicFortschritt(topic) {
|
||||
const res = await fetch(`${BASE}/topics/fortschritt?topic=${encodeURIComponent(topic)}`)
|
||||
return res.json()
|
||||
@@ -114,20 +155,6 @@ export async function deleteTopic(name) {
|
||||
await fetch(`${BASE}/topics?topic=${encodeURIComponent(name)}`, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
export async function fetchProgress(id) {
|
||||
const res = await fetch(`${BASE}/guides/${id}/progress`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function setProgress(id, chapter, done) {
|
||||
const res = await fetch(`${BASE}/guides/${id}/progress`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ chapter, done }),
|
||||
})
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function chatGuide(id, { section, outline, messages, provider = 'claude' }) {
|
||||
const res = await fetch(`${BASE}/guides/${id}/chat`, {
|
||||
method: 'POST',
|
||||
|
||||
277
frontend/src/components/BausteinPanel.vue
Normal file
277
frontend/src/components/BausteinPanel.vue
Normal 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>
|
||||
@@ -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);
|
||||
|
||||
@@ -29,7 +29,7 @@ function providerAvailable(id) {
|
||||
return p ? p.available : true
|
||||
}
|
||||
|
||||
const PROVIDER_LABELS = { claude: 'Claude', minimax: 'MiniMax', 'minimax-direkt': 'MiniMax direkt', lokal: 'Lokal' }
|
||||
const PROVIDER_LABELS = { claude: 'Claude', minimax: 'MiniMax', lokal: 'Lokal' }
|
||||
|
||||
// Tracker oben in der Navigation: Themen gesamt, pro Format erstellt/absolviert
|
||||
const trackerItems = computed(() => {
|
||||
@@ -93,8 +93,8 @@ function guideStatus(format) {
|
||||
}
|
||||
|
||||
// Schritt-Kugeln der Guide-Pipelines
|
||||
const GUIDE_STEPS = ['Auswahl', 'Auswahl-Prüfung', 'Gliederung', 'Gliederungs-Prüfung', 'Schreiben', 'Lese-Prüfung']
|
||||
const ONEPAGER_STEPS = ['Recherche', 'Recherche-Prüfung', 'Bauen', 'Prüfung']
|
||||
const GUIDE_STEPS = ['Auswahl', 'Gliederung', 'Schreiben', 'Lese-Prüfung']
|
||||
const ONEPAGER_STEPS = ['Recherche', 'Bauen', 'Prüfung']
|
||||
|
||||
// Kugeln werden wie bei den Bausteinen immer angezeigt:
|
||||
// fertig = alle grün, laufend = live, abgebrochen = Teilfortschritt, sonst grau
|
||||
@@ -102,7 +102,8 @@ function guideSteps(format) {
|
||||
const labels = format === 'OnePager' ? ONEPAGER_STEPS : GUIDE_STEPS
|
||||
const st = guideStatus(format)
|
||||
if (st === 'generating' || st === 'queued') {
|
||||
const step = props.latestByFormat[format]?.step ?? -1
|
||||
// Clamp: alte DB-Läufe können step-Werte oberhalb der neuen Listen haben
|
||||
const step = Math.min(props.latestByFormat[format]?.step ?? -1, labels.length - 1)
|
||||
return labels.map((label, i) => ({
|
||||
label,
|
||||
state: i < step ? 'done' : i === step ? 'active' : 'pending',
|
||||
@@ -112,7 +113,7 @@ function guideSteps(format) {
|
||||
return labels.map((label) => ({ label, state: 'done' }))
|
||||
}
|
||||
if (abgebrochen(format)) {
|
||||
const step = props.latestByFormat[format]?.step ?? 0
|
||||
const step = Math.min(props.latestByFormat[format]?.step ?? 0, labels.length)
|
||||
return labels.map((label, i) => ({ label, state: i < step ? 'done' : 'pending' }))
|
||||
}
|
||||
return labels.map((label) => ({ label, state: 'pending' }))
|
||||
|
||||
Reference in New Issue
Block a user