update
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)">×</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;
|
||||
|
||||
Reference in New Issue
Block a user