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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user