update
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } 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, fetchProviders, fetchStats, fetchTopicFortschritt, fetchGuideLocks, fetchGuideSteps, fetchFolders, updateQuelle as apiUpdateQuelle } from './api.js'
|
||||
import { fetchGuides, fetchTopics, createTopic as apiCreateTopic, deleteTopic as apiDeleteTopic, createGuide as apiCreate, deleteGuide, cancelGuide as apiCancel, fetchBausteineStatus, fetchActiveBausteine, createBausteine as apiCreateBausteine, resetBausteineAbStep as apiResetBausteineAbStep, cancelBausteine as apiCancelBausteine, deleteBausteine as apiDeleteBausteine, fetchProviders, fetchStats, fetchTopicFortschritt, fetchGuideLocks, fetchGuideSteps, fetchFolders, updateQuelle as apiUpdateQuelle } from './api.js'
|
||||
import { usePolling } from './composables/usePolling.js'
|
||||
import TopicSidebar from './components/TopicSidebar.vue'
|
||||
import TopicDetail from './components/TopicDetail.vue'
|
||||
@@ -21,7 +21,7 @@ const darkMode = ref(
|
||||
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
: localStorage.getItem('darkMode') === 'true',
|
||||
)
|
||||
const EMPTY_BAUSTEINE = { ready: false, generating: false, progress: null, error: null, partial: false, steps: [] }
|
||||
const EMPTY_BAUSTEINE = { ready: false, generating: false, progress: null, error: null, partial: false, steps: [], feine_steps: [] }
|
||||
const bausteine = ref({ ...EMPTY_BAUSTEINE })
|
||||
const activeBausteine = ref([])
|
||||
const provider = ref(localStorage.getItem('provider') || 'claude')
|
||||
@@ -227,12 +227,24 @@ async function handleResetBausteine() {
|
||||
await loadBausteine()
|
||||
}
|
||||
|
||||
async function handleBausteineClick({ instructions, abPhase = null }) {
|
||||
async function handleResetAbStep(step) {
|
||||
if (!selectedTopic.value) return
|
||||
uiError.value = null
|
||||
try {
|
||||
// Quelle ist hier schon festgeschrieben; nur abPhase steuert den Re-Run-Umfang.
|
||||
await apiCreateBausteine(selectedTopic.value, instructions, provider.value, undefined, undefined, abPhase)
|
||||
await apiResetBausteineAbStep(selectedTopic.value, step) // nur zurücksetzen, kein Neu-Generieren
|
||||
} catch (e) {
|
||||
uiError.value = e.message
|
||||
return
|
||||
}
|
||||
await loadBausteine()
|
||||
}
|
||||
|
||||
async function handleBausteineClick({ instructions, abPhase = null, abStep = null }) {
|
||||
if (!selectedTopic.value) return
|
||||
uiError.value = null
|
||||
try {
|
||||
// Quelle ist hier schon festgeschrieben; abPhase/abStep steuern den Re-Run-Umfang.
|
||||
await apiCreateBausteine(selectedTopic.value, instructions, provider.value, undefined, undefined, abPhase, abStep)
|
||||
} catch (e) {
|
||||
uiError.value = e.message
|
||||
return
|
||||
@@ -428,7 +440,17 @@ onMounted(async () => {
|
||||
<BausteineUebersicht
|
||||
v-if="selectedTopic && bausteineView"
|
||||
:topic="selectedTopic"
|
||||
:steps="bausteine.feine_steps || []"
|
||||
:generating="bausteine.generating"
|
||||
:progress="bausteine.progress"
|
||||
:ready="bausteine.ready"
|
||||
:partial="bausteine.partial"
|
||||
@close="bausteineView = false"
|
||||
@restartFrom="(i) => handleBausteineClick({ instructions: '', abStep: i })"
|
||||
@resetFrom="handleResetAbStep"
|
||||
@restartAll="() => handleBausteineClick({ abPhase: bausteine.ready ? 1 : null })"
|
||||
@removeAll="handleResetBausteine"
|
||||
@cancel="handleCancelBausteine"
|
||||
/>
|
||||
<ElementsOverview
|
||||
v-else-if="selectedTopic && elementsView"
|
||||
|
||||
@@ -47,11 +47,20 @@ export async function fetchBausteineStatus(topic) {
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function createBausteine(topic, instructions = '', provider = 'claude', sourceType = 'thema', sourceOrt = '', abPhase = null) {
|
||||
export async function createBausteine(topic, instructions = '', provider = 'claude', sourceType = 'thema', sourceOrt = '', abPhase = null, abStep = null) {
|
||||
const res = await fetch(`${BASE}/bausteine`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ topic, instructions, provider, source_type: sourceType, source_ort: sourceOrt, ab_phase: abPhase }),
|
||||
body: JSON.stringify({ topic, instructions, provider, source_type: sourceType, source_ort: sourceOrt, ab_phase: abPhase, ab_step: abStep }),
|
||||
})
|
||||
return jsonOrThrow(res)
|
||||
}
|
||||
|
||||
export async function resetBausteineAbStep(topic, abStep) {
|
||||
const res = await fetch(`${BASE}/bausteine/reset-step`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ topic, ab_step: abStep }),
|
||||
})
|
||||
return jsonOrThrow(res)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,41 @@ import { fetchBausteineUebersicht } from '../api.js'
|
||||
|
||||
const props = defineProps({
|
||||
topic: { type: String, required: true },
|
||||
steps: { type: Array, default: () => [] }, // feine Teilschritte {label, phase, state}
|
||||
generating: { type: Boolean, default: false },
|
||||
progress: { type: String, default: null },
|
||||
ready: { type: Boolean, default: false },
|
||||
partial: { type: Boolean, default: false },
|
||||
})
|
||||
const emit = defineEmits(['close'])
|
||||
const emit = defineEmits(['close', 'restartFrom', 'resetFrom', 'restartAll', 'removeAll', 'cancel'])
|
||||
|
||||
// Teilschritte nach Phase gruppieren, globalen Index für den Re-Run mitführen.
|
||||
const phasenGruppen = computed(() => {
|
||||
const out = []
|
||||
props.steps.forEach((s, i) => {
|
||||
const last = out[out.length - 1]
|
||||
if (last && last.phase === s.phase) last.schritte.push({ ...s, idx: i })
|
||||
else out.push({ phase: s.phase, schritte: [{ ...s, idx: i }] })
|
||||
})
|
||||
return out
|
||||
})
|
||||
|
||||
const gewaehlt = ref(null) // markierter Startpunkt (Schritt-Index)
|
||||
const confirm = ref(null) // welche destruktive Aktion gerade „Sicher?" zeigt
|
||||
const gewaehltLabel = computed(() => props.steps[gewaehlt.value]?.label || '')
|
||||
|
||||
function stepKlick(idx) {
|
||||
if (props.generating) return
|
||||
gewaehlt.value = gewaehlt.value === idx ? null : idx
|
||||
confirm.value = null
|
||||
}
|
||||
// 2-Klick-Bestätigung für destruktive Aktionen: erster Klick „armt", zweiter führt aus.
|
||||
function arm(aktion, fn) {
|
||||
if (confirm.value === aktion) { confirm.value = null; fn() }
|
||||
else confirm.value = aktion
|
||||
}
|
||||
function neuAbHier() { const i = gewaehlt.value; gewaehlt.value = null; confirm.value = null; emit('restartFrom', i) }
|
||||
function loeschAbHier() { const i = gewaehlt.value; gewaehlt.value = null; confirm.value = null; emit('resetFrom', i) }
|
||||
|
||||
const items = ref([])
|
||||
const loading = ref(true)
|
||||
@@ -62,6 +95,46 @@ const subTotal = computed(() => items.value.reduce((n, b) => n + (b.subbausteine
|
||||
<button class="bk-close" title="Schließen" @click="emit('close')">✕</button>
|
||||
</header>
|
||||
|
||||
<section v-if="steps.length" class="bk-steps">
|
||||
<div class="bk-steps-top">
|
||||
<div v-if="progress" class="bk-progress"><span class="bk-progress-dot"></span>{{ progress }}</div>
|
||||
<div v-if="!generating" class="bk-global-actions">
|
||||
<button class="bk-act play" @click="emit('restartAll')">{{ partial ? 'Fortsetzen' : ready ? 'Neu generieren' : 'Generieren' }}</button>
|
||||
<button
|
||||
v-if="ready || partial"
|
||||
class="bk-act danger"
|
||||
:class="{ armed: confirm === 'remove' }"
|
||||
@click="arm('remove', () => emit('removeAll'))"
|
||||
>{{ confirm === 'remove' ? 'Sicher?' : 'Entfernen' }}</button>
|
||||
</div>
|
||||
<div v-else class="bk-global-actions">
|
||||
<button class="bk-act danger" @click="emit('cancel')">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bk-phasen">
|
||||
<div v-for="g in phasenGruppen" :key="g.phase" class="bk-phase">
|
||||
<span class="bk-phase-label">{{ g.phase }}</span>
|
||||
<div class="bk-schritte">
|
||||
<button
|
||||
v-for="s in g.schritte"
|
||||
:key="s.idx"
|
||||
class="bk-step"
|
||||
:class="[s.state, { sel: gewaehlt === s.idx }]"
|
||||
:disabled="generating"
|
||||
:title="`Startpunkt «${s.label}» wählen`"
|
||||
@click="stepKlick(s.idx)"
|
||||
>{{ s.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="gewaehlt !== null && !generating" class="bk-step-actions">
|
||||
<span class="bk-step-actions-label">Ab «{{ gewaehltLabel }}»:</span>
|
||||
<button class="bk-act play" @click="neuAbHier">↻ neu generieren</button>
|
||||
<button class="bk-act danger" :class="{ armed: confirm === 'reset' }" @click="arm('reset', loeschAbHier)">{{ confirm === 'reset' ? 'Sicher?' : '✕ alles löschen' }}</button>
|
||||
<button class="bk-act ghost" @click="gewaehlt = null; confirm = null">Abbrechen</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div v-if="loading" class="bk-empty-state">Lade…</div>
|
||||
<div v-else-if="error" class="bk-empty-state">{{ error }}</div>
|
||||
<div v-else-if="!items.length" class="bk-empty-state">Noch keine Bausteine.</div>
|
||||
@@ -126,6 +199,93 @@ const subTotal = computed(() => items.value.reduce((n, b) => n + (b.subbausteine
|
||||
}
|
||||
.bk-close:hover { border-color: var(--accent); }
|
||||
|
||||
/* Schritt-Übersicht über den Bausteinen */
|
||||
.bk-steps {
|
||||
padding: 0.85rem 2rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--panel-soft);
|
||||
}
|
||||
.bk-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.84rem;
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.7rem;
|
||||
}
|
||||
.bk-progress-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
animation: bk-pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes bk-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
|
||||
|
||||
.bk-phasen { display: flex; flex-wrap: wrap; gap: 0.5rem 1.1rem; }
|
||||
.bk-phase { display: flex; flex-direction: column; gap: 0.3rem; }
|
||||
.bk-phase-label {
|
||||
font-size: 0.62rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
.bk-schritte { display: flex; flex-wrap: wrap; gap: 0.3rem; }
|
||||
.bk-step {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 6px;
|
||||
background: var(--panel);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.74rem;
|
||||
padding: 0.22rem 0.5rem;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bk-step:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
|
||||
.bk-step:disabled { cursor: default; opacity: 0.7; }
|
||||
.bk-step.done { border-color: var(--success-border); color: var(--success); }
|
||||
.bk-step.active { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); font-weight: 600; }
|
||||
.bk-step.pending { color: var(--text-faint); }
|
||||
.bk-step.sel { border-color: var(--accent); color: var(--on-accent); background: var(--accent); font-weight: 700; box-shadow: 0 0 0 2px var(--accent-soft); }
|
||||
|
||||
/* Kopf: Fortschritt links, globale Buttons rechts */
|
||||
.bk-steps-top { display: flex; align-items: center; gap: 1rem; min-height: 1.9rem; margin-bottom: 0.7rem; }
|
||||
.bk-steps-top .bk-progress { margin-bottom: 0; }
|
||||
.bk-global-actions { margin-left: auto; display: flex; gap: 0.4rem; }
|
||||
|
||||
/* Aktions-Leiste bei gewähltem Startpunkt */
|
||||
.bk-step-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.7rem;
|
||||
border-top: 1px dashed var(--border-strong);
|
||||
}
|
||||
.bk-step-actions-label { font-size: 0.8rem; font-weight: 600; color: var(--text-muted); }
|
||||
|
||||
.bk-act {
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 6px;
|
||||
background: var(--panel);
|
||||
color: var(--text);
|
||||
font-size: 0.8rem;
|
||||
padding: 0.3rem 0.7rem;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
.bk-act:hover { border-color: var(--accent); }
|
||||
.bk-act.play { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
|
||||
.bk-act.play:hover { background: var(--accent-hover); }
|
||||
.bk-act.danger { color: var(--danger); border-color: var(--danger); background: transparent; }
|
||||
.bk-act.danger.armed { background: var(--danger); color: #fff; }
|
||||
.bk-act.ghost { color: var(--text-muted); }
|
||||
|
||||
.bk-empty-state {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
@@ -70,16 +70,7 @@ const bausteineState = computed(() => {
|
||||
return props.bausteine.ready ? 'done' : 'none'
|
||||
})
|
||||
|
||||
// Re-Run ab gewählter Phase (1-basiert). null = ab Anfang / Fortsetzen.
|
||||
const gewaehltePhase = ref(null)
|
||||
// Phasen anklickbar, sobald etwas (teil-)gebaut ist und gerade nicht generiert wird.
|
||||
const phasenWaehlbar = computed(() => (props.bausteine.ready || props.bausteine.partial) && !props.bausteine.generating)
|
||||
const gewaehltesLabel = computed(() => (props.bausteine.steps || [])[(gewaehltePhase.value || 0) - 1]?.label || '')
|
||||
|
||||
function phaseKlick(n) {
|
||||
if (!phasenWaehlbar.value) return
|
||||
gewaehltePhase.value = gewaehltePhase.value === n ? null : n
|
||||
}
|
||||
// Re-Run ab Teilschritt passiert jetzt in der Bausteine-Übersicht; die Sidebar hat keine Phasen-Pillen mehr.
|
||||
|
||||
// Nur FREMDE Themen — das gewählte Thema zeigt seinen Fortschritt inline an der Zeile
|
||||
const activeGenerations = computed(() => {
|
||||
@@ -104,10 +95,9 @@ function confirmResetBausteine() {
|
||||
|
||||
function handleBausteinePlay() {
|
||||
if (bausteineState.value === 'generating') return
|
||||
// Nur „Neu generieren" (ready) nutzt eine Phase; Fortsetzen/Erstbau resumen ohne Löschen.
|
||||
const abPhase = props.bausteine.ready ? (gewaehltePhase.value ?? 1) : null
|
||||
// „Neu generieren" (ready) = ab Anfang; Erstbau/Fortsetzen ohne Löschen. Feiner Re-Run via Übersicht.
|
||||
const abPhase = props.bausteine.ready ? 1 : null
|
||||
emit('bausteineClick', { instructions: '', abPhase })
|
||||
gewaehltePhase.value = null
|
||||
}
|
||||
|
||||
// Name-Klick = Primäraktion: fertige Bausteine → Übersicht, sonst Panel auf/zu.
|
||||
@@ -389,16 +379,6 @@ function saveQuelle() {
|
||||
class="resume-badge"
|
||||
title="Abgebrochen — Fortsetzen möglich"
|
||||
>Pausiert</span>
|
||||
<span class="step-dots">
|
||||
<span
|
||||
v-for="(s, i) in (bausteine.steps || [])"
|
||||
:key="s.label"
|
||||
class="step-pill"
|
||||
:class="[s.state, { sel: gewaehltePhase === i + 1, klickbar: phasenWaehlbar }]"
|
||||
:title="(s.state === 'active' ? (bausteine.progress || s.label) : s.label) + (phasenWaehlbar ? ' — Klick: ab hier neu' : '')"
|
||||
@click.stop="phaseKlick(i + 1)"
|
||||
>{{ i + 1 }}</span>
|
||||
</span>
|
||||
</button>
|
||||
<button class="panel-toggle" :class="{ open: isOpen('bausteine') }" title="Aktionen" @click.stop="togglePanel('bausteine')">▾</button>
|
||||
</div>
|
||||
@@ -407,7 +387,7 @@ function saveQuelle() {
|
||||
<button class="panel-btn danger" :class="{ armed: pendingConfirm === 'bausteine' }" @click="confirmCancelBausteine">{{ pendingConfirm === 'bausteine' ? 'Sicher?' : 'Abbrechen' }}</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button class="panel-btn play" @click="handleBausteinePlay">{{ bausteine.partial ? 'Fortsetzen' : bausteine.ready ? (gewaehltePhase ? `Ab «${gewaehltesLabel}» neu` : 'Neu generieren') : 'Generieren' }}</button>
|
||||
<button class="panel-btn play" @click="handleBausteinePlay">{{ bausteine.partial ? 'Fortsetzen' : bausteine.ready ? 'Neu generieren' : 'Generieren' }}</button>
|
||||
<button
|
||||
v-if="bausteine.ready || bausteine.partial"
|
||||
class="panel-btn danger"
|
||||
|
||||
Reference in New Issue
Block a user