This commit is contained in:
Team3
2026-06-07 11:29:04 +02:00
parent 6743b1234e
commit 58fb1b6a56
16 changed files with 225 additions and 60 deletions

View File

@@ -1,5 +1,5 @@
<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
import { fetchGuides, fetchTopics, createTopic as apiCreateTopic, deleteTopic as apiDeleteTopic, createGuide as apiCreate, deleteGuide, cancelGuide as apiCancel, fetchBausteineStatus, fetchActiveBausteine, createBausteine as apiCreateBausteine, cancelBausteine as apiCancelBausteine, deleteBausteine as apiDeleteBausteine, fetchProjects, deleteProject as apiDeleteProject, fetchProviders, fetchStats } from './api.js'
import TopicSidebar from './components/TopicSidebar.vue'
import TopicDetail from './components/TopicDetail.vue'
@@ -118,9 +118,22 @@ async function loadTopics() {
}
}
// Fehlermeldungen verhalten sich wie Flash-Messages: × blendet aus,
// beim Reload sind Alt-Fehler von vornherein ausgeblendet.
const dismissedErrors = ref(new Set())
let errorsInitialized = false
function handleDismissError(guideId) {
dismissedErrors.value = new Set([...dismissedErrors.value, guideId])
}
async function loadGuides() {
try {
guides.value = await fetchGuides()
if (!errorsInitialized) {
errorsInitialized = true
dismissedErrors.value = new Set(guides.value.filter((g) => g.status === 'error').map((g) => g.id))
}
loadStats()
} catch (e) {
console.error('Fehler beim Laden:', e)
@@ -166,10 +179,16 @@ function selectTopic(topic) {
selectedTopic.value = topic
previewGuide.value = null
sidebarSticky.value = false
localStorage.setItem('lastTopic', topic)
loadBausteine()
nextTick(autoPreview)
}
// Beim Reload dort landen, wo man vorher war (Thema + Format)
watch(previewGuide, (g) => {
if (g) localStorage.setItem('lastFormat', g.format)
})
async function createTopic(topic) {
await apiCreateTopic(topic)
await loadTopics()
@@ -223,8 +242,8 @@ function handlePreview(guide) {
previewGuide.value = guide
}
async function handleDeleteGuide(guideId) {
await deleteGuide(guideId)
async function handleDeleteGuide(guideId, slots = false) {
await deleteGuide(guideId, slots)
if (previewGuide.value?.id === guideId) {
previewGuide.value = null
}
@@ -278,7 +297,14 @@ function onVisibility() {
onMounted(async () => {
await Promise.all([loadGuides(), loadTopics(), loadProjects(), loadProviders()])
if (!selectedTopic.value && topics.value.length) {
const savedTopic = localStorage.getItem('lastTopic')
const savedFormat = localStorage.getItem('lastFormat')
if (savedTopic && [...topics.value, ...projectNames.value].includes(savedTopic)) {
selectTopic(savedTopic)
await nextTick()
const g = doneByFormat.value[savedFormat]
if (g) previewGuide.value = g
} else if (!selectedTopic.value && topics.value.length) {
selectTopic(topics.value[0])
}
document.addEventListener('visibilitychange', onVisibility)
@@ -302,6 +328,7 @@ onUnmounted(() => {
:doneByFormat="doneByFormat"
:latestByFormat="latestByFormat"
:allGuides="guides"
:dismissedErrors="dismissedErrors"
:bausteine="bausteine"
:activeBausteine="activeBausteine"
:pinned="sidebarPinned"
@@ -320,6 +347,7 @@ onUnmounted(() => {
@deleteProject="handleDeleteProject"
@cancelGuide="handleCancel"
@deleteGuide="handleDeleteGuide"
@dismissError="handleDismissError"
@preview="handlePreview"
@togglePin="toggleSidebarPin"
@sidebarLeave="onSidebarLeave"

View File

@@ -64,8 +64,8 @@ export async function cancelGuide(id) {
await fetch(`${BASE}/guides/${id}/cancel`, { method: 'POST' })
}
export async function deleteGuide(id) {
await fetch(`${BASE}/guides/${id}`, { method: 'DELETE' })
export async function deleteGuide(id, slots = false) {
await fetch(`${BASE}/guides/${id}${slots ? '?slots=1' : ''}`, { method: 'DELETE' })
}
export async function fetchGuideContent(id) {

View File

@@ -48,6 +48,7 @@ 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)
@@ -55,6 +56,7 @@ watch(() => props.previewGuide?.id, loadContent, { immediate: true })
async function loadContent() {
content.value = null
loadError.value = null
doneChapters.value = new Set()
const g = props.previewGuide
if (!g || g.status !== 'done') return
@@ -62,6 +64,7 @@ async function loadContent() {
content.value = await fetchGuideContent(g.id)
} catch (e) {
console.error('Fehler beim Laden des Guides:', e)
loadError.value = 'Inhalt nicht verfügbar — die Datei fehlt. Guide neu generieren (▶).'
return
}
try {
@@ -236,7 +239,7 @@ async function send() {
</div>
<div v-else-if="previewGuide" class="empty-preview">
<p>Lade Inhalt</p>
<p>{{ loadError || 'Lade Inhalt…' }}</p>
</div>
<div class="empty-preview" v-else>

View File

@@ -9,6 +9,7 @@ const props = defineProps({
doneByFormat: { type: Object, default: () => ({}) },
latestByFormat: { type: Object, default: () => ({}) },
allGuides: { type: Array, default: () => [] },
dismissedErrors: { type: Object, default: () => new Set() },
bausteine: { type: Object, default: () => ({ ready: false, generating: false, progress: null, error: null }) },
activeBausteine: { type: Array, default: () => [] },
pinned: { type: Boolean, default: true },
@@ -17,7 +18,7 @@ const props = defineProps({
providers: { type: Array, default: () => [] },
})
const emit = defineEmits(['select', 'create', 'formatClick', 'bausteineClick', 'cancelBausteine', 'resetBausteine', 'deleteTopic', 'deleteProject', 'cancelGuide', 'deleteGuide', 'preview', 'togglePin', 'sidebarLeave', 'toggleDark', 'setProvider'])
const emit = defineEmits(['select', 'create', 'formatClick', 'bausteineClick', 'cancelBausteine', 'resetBausteine', 'deleteTopic', 'deleteProject', 'cancelGuide', 'deleteGuide', 'dismissError', 'preview', 'togglePin', 'sidebarLeave', 'toggleDark', 'setProvider'])
function providerAvailable(id) {
const p = props.providers.find((x) => x.id === id)
@@ -104,21 +105,38 @@ function guideStatus(format) {
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']
// Kugeln werden wie bei den Bausteinen immer angezeigt:
// fertig = alle grün, laufend = live, abgebrochen = Teilfortschritt, sonst grau
function guideSteps(format) {
const st = guideStatus(format)
if (st !== 'generating' && st !== 'queued') return []
const labels = format === 'OnePager' ? ONEPAGER_STEPS : GUIDE_STEPS
const step = props.latestByFormat[format]?.step ?? -1
return labels.map((label, i) => ({
label,
state: i < step ? 'done' : i === step ? 'active' : 'pending',
}))
const st = guideStatus(format)
if (st === 'generating' || st === 'queued') {
const step = props.latestByFormat[format]?.step ?? -1
return labels.map((label, i) => ({
label,
state: i < step ? 'done' : i === step ? 'active' : 'pending',
}))
}
if (props.doneByFormat[format]) {
return labels.map((label) => ({ label, state: 'done' }))
}
if (abgebrochen(format)) {
const step = props.latestByFormat[format]?.step ?? 0
return labels.map((label, i) => ({ label, state: i < step ? 'done' : 'pending' }))
}
return labels.map((label) => ({ label, state: 'pending' }))
}
function errorMsg(format) {
const latest = props.latestByFormat[format]
if (latest?.status === 'error') return latest.error_msg || 'Fehler bei der Generierung'
return ''
if (latest?.status !== 'error' || props.dismissedErrors.has(latest.id)) return ''
return latest.error_msg || 'Fehler bei der Generierung'
}
// Abgebrochener Lauf = Teilfortschritt vorhanden: ▶ setzt fort, ✕ löscht den Fortschritt
function abgebrochen(format) {
const latest = props.latestByFormat[format]
return latest?.status === 'error' && (latest.error_msg || '').startsWith('Abgebrochen')
}
function handleFormatClick(format) {
@@ -150,11 +168,10 @@ function handlePlay(format) {
emit('formatClick', { format, instructions: '' })
}
// Flash-Message-Verhalten: × blendet nur aus, nichts wird gelöscht
function dismissError(format) {
const latest = props.latestByFormat[format]
if (latest?.status === 'error') {
emit('deleteGuide', latest.id)
}
if (latest?.status === 'error') emit('dismissError', latest.id)
}
function handleDelete(format) {
@@ -297,7 +314,7 @@ function confirmDeleteProject(name) {
<template v-if="guideStatus(f.key) !== 'generating' && guideStatus(f.key) !== 'queued'">
<button
class="action-btn play"
:title="playLock(f.key) || 'Generieren'"
:title="playLock(f.key) || (abgebrochen(f.key) ? 'Fortsetzen' : 'Generieren')"
:disabled="!!playLock(f.key)"
@click="handlePlay(f.key)"
></button>
@@ -306,7 +323,7 @@ function confirmDeleteProject(name) {
</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="Fehler entfernen" @click="dismissError(f.key)">&times;</button>
<button class="format-error-x" title="Ausblenden" @click="dismissError(f.key)">×</button>
</div>
</div>
</div>
@@ -664,6 +681,7 @@ function confirmDeleteProject(name) {
}
.format-x.armed,
.format-error-x.armed,
.delete-topic.armed {
display: inline-block;
font-size: 0.7rem;