Frontend: locks vom Backend, uiError sichtbar, Pausiert-Badge, Inline-Progress, Mobile-Exklusivität, Fehler-Persistenz

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
team3
2026-06-12 08:18:24 +02:00
parent 2c426e6ac4
commit 700ba1e0e8
4 changed files with 133 additions and 39 deletions

View File

@@ -8,6 +8,8 @@ const props = defineProps({
selectedTopic: { type: String, default: null },
stats: { type: Object, default: null },
fortschritt: { type: Object, default: () => ({}) },
locks: { type: Object, default: () => ({}) },
uiError: { type: String, default: null },
doneByFormat: { type: Object, default: () => ({}) },
latestByFormat: { type: Object, default: () => ({}) },
allGuides: { type: Array, default: () => [] },
@@ -20,7 +22,7 @@ const props = defineProps({
providers: { type: Array, default: () => [] },
})
const emit = defineEmits(['select', 'create', 'formatClick', 'bausteineClick', 'cancelBausteine', 'resetBausteine', 'deleteTopic', 'deleteProject', 'cancelGuide', 'deleteGuide', 'dismissError', 'preview', 'openElements', 'togglePin', 'sidebarLeave', 'toggleDark', 'setProvider'])
const emit = defineEmits(['select', 'create', 'formatClick', 'bausteineClick', 'cancelBausteine', 'resetBausteine', 'deleteTopic', 'deleteProject', 'cancelGuide', 'deleteGuide', 'dismissError', 'dismissUiError', 'preview', 'openElements', 'togglePin', 'sidebarLeave', 'toggleDark', 'setProvider'])
function providerAvailable(id) {
const p = props.providers.find((x) => x.id === id)
@@ -54,12 +56,13 @@ const bausteineState = computed(() => {
return props.bausteine.ready ? 'done' : 'none'
})
// Nur FREMDE Themen — das gewählte Thema zeigt seinen Fortschritt inline an der Zeile
const activeGenerations = computed(() => {
const bausteinLines = props.activeBausteine.map(
(b) => `${b.topic} Bausteine: ${b.progress || 'Wartend…'}`,
)
const bausteinLines = props.activeBausteine
.filter((b) => b.topic !== props.selectedTopic)
.map((b) => `${b.topic} Bausteine: ${b.progress || 'Wartend…'}`)
const guideLines = props.allGuides
.filter((g) => g.status === 'generating' || g.status === 'queued')
.filter((g) => (g.status === 'generating' || g.status === 'queued') && g.topic !== props.selectedTopic)
.map((g) => `${g.topic} ${g.format}: ${g.progress || 'Wartend…'}`)
return [...bausteinLines, ...guideLines]
})
@@ -118,6 +121,7 @@ function guideSteps(format) {
function errorMsg(format) {
const latest = props.latestByFormat[format]
if (latest?.status !== 'error' || props.dismissedErrors.has(latest.id)) return ''
if (abgebrochen(format)) return '' // kein roter Fehler — das Pausiert-Badge zeigt den Zustand
return latest.error_msg || 'Fehler bei der Generierung'
}
@@ -134,28 +138,11 @@ function handleFormatClick(format) {
}
}
// Lernschulden-Regeln (nur Neu-Erstellungen; Resume + Neu-Generieren bestehender erlaubt):
// Progression pro Thema (MiniGuide → Guide → FullGuide) + max. 3 offene je Format.
const VORSTUFE = { Guide: 'MiniGuide', FullGuide: 'Guide' }
function offeneGuides(format) {
const f = props.stats?.formate?.[format]
return (f?.erstellt ?? 0) - (f?.absolviert ?? 0)
}
// Sperr-Gründe kommen vom Backend (GET /guides/locks) — die Regeln existieren
// nur noch dort. Solange locks noch nicht geladen sind: Button frei, das
// Backend weist ungültige Starts ohnehin ab (sichtbar über uiError).
function playLock(format) {
if (format === 'OnePager') return null
if (!props.bausteine.ready) return 'Erst Bausteine erstellen'
if (props.doneByFormat[format] || abgebrochen(format)) return null
const vorstufe = VORSTUFE[format]
if (vorstufe && !props.fortschritt?.[vorstufe]) {
return `Erst den ${vorstufe} dieses Themas absolvieren`
}
const offen = offeneGuides(format)
if (offen >= 3) {
return `Erst ${format}s absolvieren — ${offen} offen (max. 3)`
}
return null
return props.locks?.[format] ?? null
}
function handlePlay(format) {
@@ -240,12 +227,21 @@ function confirmDeleteProject(name) {
>{{ PROVIDER_LABELS[p.id] || p.id }}</button>
</div>
<div class="format-section" v-if="selectedTopic">
<div class="format-error ui-error" v-if="uiError">
<span class="format-error-text">{{ uiError }}</span>
<button class="format-error-x" title="Ausblenden" @click="emit('dismissUiError')">×</button>
</div>
<div class="progress-info" v-if="activeGenerations.length">
<div v-for="(line, i) in activeGenerations" :key="i">{{ line }}</div>
</div>
<div class="format-row bausteine-row ord-bausteine">
<div class="format-name bausteine-name">
<span class="format-label">Bausteine</span>
<span
v-if="bausteine.partial && bausteineState !== 'generating'"
class="resume-badge"
title="Abgebrochen — ▶ setzt fort"
>Pausiert</span>
<span class="step-dots">
<span
v-for="s in (bausteine.steps || [])"
@@ -280,7 +276,10 @@ function confirmDeleteProject(name) {
</template>
</div>
</div>
<div v-if="bausteine.error" class="format-error ord-bausteine">
<div v-if="bausteineState === 'generating'" class="format-progress ord-bausteine">
{{ bausteine.progress || 'Wartend' }}
</div>
<div v-if="bausteine.error && !bausteine.error.startsWith('Abgebrochen')" class="format-error ord-bausteine">
<span class="format-error-text">{{ bausteine.error }}</span>
</div>
<!-- OnePager (unabhängig von Bausteinen) steht per CSS-order vor der Bausteine-Zeile -->
@@ -288,6 +287,11 @@ function confirmDeleteProject(name) {
<div :class="['format-row', 'fmt-' + guideStatus(f.key)]">
<button class="format-name" @click="handleFormatClick(f.key)">
<span class="format-label">{{ f.label }}</span>
<span
v-if="abgebrochen(f.key)"
class="resume-badge"
title="Abgebrochen — ▶ setzt fort"
>Pausiert</span>
<span class="step-dots" v-if="guideSteps(f.key).length">
<span
v-for="s in guideSteps(f.key)"
@@ -316,6 +320,10 @@ function confirmDeleteProject(name) {
</template>
</div>
</div>
<div
v-if="guideStatus(f.key) === 'generating' || guideStatus(f.key) === 'queued'"
class="format-progress"
>{{ latestByFormat[f.key]?.progress || 'Wartend…' }}</div>
<div v-if="errorMsg(f.key)" class="format-error">
<span class="format-error-text">{{ errorMsg(f.key) }}</span>
<button class="format-error-x" title="Ausblenden" @click="dismissError(f.key)">×</button>
@@ -653,6 +661,35 @@ function confirmDeleteProject(name) {
animation: pulse 1.5s ease-in-out infinite;
}
/* Abgewiesene Aktion (409/400) — oberhalb aller Format-Zeilen */
.ui-error {
order: 0;
padding: 0.4rem 0.75rem;
background: var(--warning-soft);
}
/* Fortschritts-Text direkt unter der laufenden Format-Zeile */
.format-progress {
padding: 0 0.75rem 5px calc(0.75rem + 8px);
font-size: 0.72rem;
color: var(--warning);
line-height: 1.3;
animation: pulse 1.5s ease-in-out infinite;
}
.resume-badge {
flex: 0 0 auto;
font-size: 0.6rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--warning);
background: var(--warning-soft);
border: 1px solid var(--warning-border);
border-radius: 4px;
padding: 1px 5px;
}
.format-row {
display: flex;
align-items: center;