update
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
>×</span>
|
||||
<span
|
||||
v-else-if="bausteine.partial"
|
||||
class="format-x"
|
||||
title="Fortschritt löschen"
|
||||
@click.stop="confirmResetBausteine"
|
||||
>×</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;
|
||||
|
||||
Reference in New Issue
Block a user