This commit is contained in:
Team3
2026-06-06 16:07:04 +02:00
parent 18bb18bf4a
commit 4aa3130807
19 changed files with 861 additions and 206 deletions

View File

@@ -1,17 +1,12 @@
<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { fetchGuides, fetchTopics, createGuide as apiCreate, deleteGuide, cancelGuide as apiCancel, fetchBausteineStatus, fetchActiveBausteine, createBausteine as apiCreateBausteine, deleteBausteine as apiDeleteBausteine, fetchProjects, deleteProject as apiDeleteProject, fetchProviders } from './api.js'
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 } from './api.js'
import TopicSidebar from './components/TopicSidebar.vue'
import TopicDetail from './components/TopicDetail.vue'
const guides = ref([])
const projects = ref([])
const manualTopics = ref(JSON.parse(localStorage.getItem('manualTopics') || '[]'))
const backendTopics = ref([])
function persistManualTopics() {
localStorage.setItem('manualTopics', JSON.stringify(manualTopics.value))
}
const selectedTopic = ref(null)
const previewGuide = ref(null)
const sidebarPinned = ref(localStorage.getItem('sidebarPinned') !== 'false')
@@ -21,7 +16,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 }
const EMPTY_BAUSTEINE = { ready: false, generating: false, progress: null, error: null, partial: false, steps: [] }
const bausteine = ref({ ...EMPTY_BAUSTEINE })
const activeBausteine = ref([])
const provider = ref(localStorage.getItem('provider') || 'claude')
@@ -76,18 +71,7 @@ const projectNames = computed(() => projects.value.map((p) => p.name))
const topics = computed(() => {
const isProject = new Set(projectNames.value)
const topicDates = {}
for (const g of guides.value) {
if (isProject.has(g.topic)) continue
if (!topicDates[g.topic] || g.created_at > topicDates[g.topic]) {
topicDates[g.topic] = g.created_at
}
}
for (const t of [...backendTopics.value, ...manualTopics.value]) {
if (isProject.has(t)) continue
if (!topicDates[t]) topicDates[t] = ''
}
return Object.keys(topicDates).sort((a, b) => topicDates[b].localeCompare(topicDates[a]))
return backendTopics.value.filter((t) => !isProject.has(t))
})
const doneByFormat = computed(() => {
@@ -176,16 +160,26 @@ function selectTopic(topic) {
nextTick(autoPreview)
}
function createTopic(topic) {
if (!manualTopics.value.includes(topic)) {
manualTopics.value.push(topic)
persistManualTopics()
}
async function createTopic(topic) {
await apiCreateTopic(topic)
await loadTopics()
selectedTopic.value = topic
previewGuide.value = null
loadBausteine()
}
async function handleCancelBausteine() {
if (!selectedTopic.value) return
await apiCancelBausteine(selectedTopic.value)
await loadBausteine()
}
async function handleResetBausteine() {
if (!selectedTopic.value) return
await apiDeleteBausteine(selectedTopic.value)
await loadBausteine()
}
async function handleBausteineClick({ instructions }) {
if (!selectedTopic.value) return
await apiCreateBausteine(selectedTopic.value, instructions, provider.value)
@@ -247,8 +241,7 @@ async function handleDeleteTopic(topic) {
await deleteGuide(g.id)
}
await apiDeleteBausteine(topic)
manualTopics.value = manualTopics.value.filter((t) => t !== topic)
persistManualTopics()
await apiDeleteTopic(topic)
await loadTopics()
if (selectedTopic.value === topic) {
selectedTopic.value = null
@@ -304,6 +297,8 @@ onUnmounted(() => {
@create="createTopic"
@formatClick="handleFormatClick"
@bausteineClick="handleBausteineClick"
@cancelBausteine="handleCancelBausteine"
@resetBausteine="handleResetBausteine"
@deleteTopic="handleDeleteTopic"
@deleteProject="handleDeleteProject"
@cancelGuide="handleCancel"

View File

@@ -33,6 +33,10 @@ export async function createBausteine(topic, instructions = '', provider = 'clau
return res.json()
}
export async function cancelBausteine(topic) {
await fetch(`${BASE}/bausteine/cancel?topic=${encodeURIComponent(topic)}`, { method: 'POST' })
}
export async function deleteBausteine(topic) {
await fetch(`${BASE}/bausteine?topic=${encodeURIComponent(topic)}`, { method: 'DELETE' })
}
@@ -70,6 +74,18 @@ export async function fetchTopics() {
return res.json()
}
export async function createTopic(name) {
await fetch(`${BASE}/topics`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
})
}
export async function deleteTopic(name) {
await fetch(`${BASE}/topics?topic=${encodeURIComponent(name)}`, { method: 'DELETE' })
}
export async function fetchProgress(id) {
const res = await fetch(`${BASE}/guides/${id}/progress`)
return res.json()

View File

@@ -16,7 +16,7 @@ const props = defineProps({
providers: { type: Array, default: () => [] },
})
const emit = defineEmits(['select', 'create', 'formatClick', 'bausteineClick', 'deleteTopic', 'deleteProject', 'cancelGuide', 'deleteGuide', 'preview', 'togglePin', 'sidebarLeave', 'toggleDark', 'setProvider'])
const emit = defineEmits(['select', 'create', 'formatClick', 'bausteineClick', 'cancelBausteine', 'resetBausteine', 'deleteTopic', 'deleteProject', 'cancelGuide', 'deleteGuide', 'preview', 'togglePin', 'sidebarLeave', 'toggleDark', 'setProvider'])
function providerAvailable(id) {
const p = props.providers.find((x) => x.id === id)
@@ -34,6 +34,10 @@ const formats = [
const BAUSTEINE_KEY = '__bausteine__'
const bausteineUnsortiert = computed(
() => props.bausteine.ready && props.bausteine.steps?.at(-1)?.state === 'pending',
)
const bausteineState = computed(() => {
if (props.bausteine.generating) return 'generating'
return props.bausteine.ready ? 'done' : 'none'
@@ -49,6 +53,16 @@ const activeGenerations = computed(() => {
return [...bausteinLines, ...guideLines]
})
function confirmCancelBausteine() {
if (!confirm('Aktuellen Schritt abbrechen? Bisheriger Fortschritt bleibt erhalten.')) return
emit('cancelBausteine')
}
function confirmResetBausteine() {
if (!confirm('Gespeicherten Bausteine-Fortschritt löschen?')) return
emit('resetBausteine')
}
function handleBausteinePlay() {
if (bausteineState.value === 'generating') return
const text = activeInput.value === BAUSTEINE_KEY ? inputText.value.trim() : ''
@@ -171,15 +185,36 @@ function confirmDeleteProject(name) {
<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', 'fmt-' + bausteineState]">
<button class="format-name">
<div class="format-row bausteine-row">
<div class="format-name bausteine-name">
<span class="format-label">Bausteine</span>
</button>
<span class="step-dots">
<span
v-for="s in (bausteine.steps || [])"
:key="s.label"
class="step-dot"
:class="s.state"
:title="s.state === 'active' ? (bausteine.progress || s.label) : s.label"
></span>
</span>
<span
v-if="bausteineState === 'generating'"
class="format-x"
title="Aktuellen Schritt abbrechen (Fortschritt bleibt)"
@click.stop="confirmCancelBausteine"
>&times;</span>
<span
v-else-if="bausteine.partial"
class="format-x"
title="Fortschritt löschen"
@click.stop="confirmResetBausteine"
>&times;</span>
</div>
<div class="format-actions">
<template v-if="bausteineState !== 'generating'">
<button
class="action-btn play"
:title="bausteine.ready ? 'Bausteine neu erstellen' : 'Bausteine erstellen'"
:title="bausteine.partial ? 'Fortsetzen' : bausteineUnsortiert ? 'Sortierung nachholen' : bausteine.ready ? 'Bausteine neu erstellen' : 'Bausteine erstellen'"
@click="handleBausteinePlay"
></button>
<button
@@ -216,8 +251,8 @@ function confirmDeleteProject(name) {
<template v-if="guideStatus(f.key) !== 'generating' && guideStatus(f.key) !== 'queued'">
<button
class="action-btn play"
:title="bausteine.ready ? 'Generieren' : 'Erst Bausteine erstellen'"
:disabled="!bausteine.ready"
:title="f.key === 'OnePager' || bausteine.ready ? 'Generieren' : 'Erst Bausteine erstellen'"
:disabled="f.key !== 'OnePager' && !bausteine.ready"
@click="handlePlay(f.key)"
></button>
<button
@@ -452,6 +487,41 @@ function confirmDeleteProject(name) {
margin-bottom: 0.5rem;
}
.bausteine-name {
display: flex;
align-items: center;
gap: 8px;
cursor: default;
}
.step-dots {
display: inline-flex;
gap: 5px;
flex: 1;
}
.step-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--border-strong);
flex-shrink: 0;
}
.step-dot.done {
background: var(--success-border);
}
.step-dot.active {
background: var(--warning-border);
animation: dot-pulse 1.2s ease-in-out infinite;
}
@keyframes dot-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
.action-btn:disabled {
opacity: 0.35;
cursor: not-allowed;