update
This commit is contained in:
@@ -10,8 +10,24 @@ const manualTopics = ref([])
|
||||
const selectedTopic = ref(null)
|
||||
const previewGuide = ref(null)
|
||||
const showBausteine = ref(false)
|
||||
const sidebarPinned = ref(localStorage.getItem('sidebarPinned') !== 'false')
|
||||
const sidebarSticky = ref(false)
|
||||
let pollTimer = null
|
||||
|
||||
function toggleSidebarPin() {
|
||||
sidebarPinned.value = !sidebarPinned.value
|
||||
localStorage.setItem('sidebarPinned', sidebarPinned.value ? 'true' : 'false')
|
||||
if (sidebarPinned.value) sidebarSticky.value = false
|
||||
}
|
||||
|
||||
function clickHoverZone() {
|
||||
sidebarSticky.value = !sidebarSticky.value
|
||||
}
|
||||
|
||||
function onSidebarLeave() {
|
||||
if (!sidebarPinned.value) sidebarSticky.value = false
|
||||
}
|
||||
|
||||
const topics = computed(() => {
|
||||
const topicDates = {}
|
||||
for (const g of guides.value) {
|
||||
@@ -25,7 +41,19 @@ const topics = computed(() => {
|
||||
return Object.keys(topicDates).sort((a, b) => topicDates[b].localeCompare(topicDates[a]))
|
||||
})
|
||||
|
||||
const guidesByFormat = computed(() => {
|
||||
const doneByFormat = computed(() => {
|
||||
const map = {}
|
||||
for (const g of guides.value) {
|
||||
if (g.topic !== selectedTopic.value) continue
|
||||
if (g.status !== 'done') continue
|
||||
if (!map[g.format] || g.created_at > map[g.format].created_at) {
|
||||
map[g.format] = g
|
||||
}
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const latestByFormat = computed(() => {
|
||||
const map = {}
|
||||
for (const g of guides.value) {
|
||||
if (g.topic !== selectedTopic.value) continue
|
||||
@@ -51,9 +79,9 @@ async function loadGuides() {
|
||||
const FORMAT_ORDER = ['OnePager', 'Cheatsheet', 'MiniGuide', 'BeginnerGuide', 'IntermediateGuide', 'ExtendedGuide']
|
||||
|
||||
function autoPreview() {
|
||||
const map = guidesByFormat.value
|
||||
const map = doneByFormat.value
|
||||
for (const f of FORMAT_ORDER) {
|
||||
if (map[f]?.status === 'done') {
|
||||
if (map[f]) {
|
||||
previewGuide.value = map[f]
|
||||
return
|
||||
}
|
||||
@@ -164,13 +192,16 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout">
|
||||
<div class="layout" :class="{ 'sidebar-floating': !sidebarPinned, 'sidebar-open': sidebarSticky }">
|
||||
<div v-if="!sidebarPinned" class="hover-zone" @click="clickHoverZone"></div>
|
||||
<TopicSidebar
|
||||
:topics="topics"
|
||||
:selectedTopic="selectedTopic"
|
||||
:guidesByFormat="guidesByFormat"
|
||||
:doneByFormat="doneByFormat"
|
||||
:latestByFormat="latestByFormat"
|
||||
:allGuides="guides"
|
||||
:bausteineActive="showBausteine"
|
||||
:pinned="sidebarPinned"
|
||||
@select="selectTopic"
|
||||
@create="createTopic"
|
||||
@formatClick="handleFormatClick"
|
||||
@@ -180,6 +211,8 @@ onUnmounted(() => {
|
||||
@preview="handlePreview"
|
||||
@rework="handleRework"
|
||||
@showBausteine="handleShowBausteine"
|
||||
@togglePin="toggleSidebarPin"
|
||||
@sidebarLeave="onSidebarLeave"
|
||||
/>
|
||||
<BausteineView
|
||||
v-if="selectedTopic && showBausteine"
|
||||
@@ -212,6 +245,34 @@ body {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hover-zone {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 50px;
|
||||
height: 100vh;
|
||||
z-index: 5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.layout.sidebar-floating > .sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.2s ease;
|
||||
z-index: 10;
|
||||
box-shadow: 0 0 16px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.layout.sidebar-floating .hover-zone:hover ~ .sidebar,
|
||||
.layout.sidebar-floating > .sidebar:hover,
|
||||
.layout.sidebar-floating.sidebar-open > .sidebar {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.empty-main {
|
||||
|
||||
@@ -4,12 +4,14 @@ import { ref, computed } from 'vue'
|
||||
const props = defineProps({
|
||||
topics: { type: Array, required: true },
|
||||
selectedTopic: { type: String, default: null },
|
||||
guidesByFormat: { type: Object, default: () => ({}) },
|
||||
doneByFormat: { type: Object, default: () => ({}) },
|
||||
latestByFormat: { type: Object, default: () => ({}) },
|
||||
allGuides: { type: Array, default: () => [] },
|
||||
bausteineActive: { type: Boolean, default: false },
|
||||
pinned: { type: Boolean, default: true },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['select', 'create', 'formatClick', 'deleteTopic', 'cancelGuide', 'deleteGuide', 'preview', 'rework', 'showBausteine'])
|
||||
const emit = defineEmits(['select', 'create', 'formatClick', 'deleteTopic', 'cancelGuide', 'deleteGuide', 'preview', 'rework', 'showBausteine', 'togglePin', 'sidebarLeave'])
|
||||
|
||||
const formats = [
|
||||
{ key: 'OnePager', label: 'OnePager' },
|
||||
@@ -27,14 +29,22 @@ const activeGenerations = computed(() => {
|
||||
})
|
||||
|
||||
function guideStatus(format) {
|
||||
const guide = props.guidesByFormat[format]
|
||||
if (!guide) return 'none'
|
||||
return guide.status
|
||||
if (props.doneByFormat[format]) return 'done'
|
||||
const latest = props.latestByFormat[format]
|
||||
if (!latest) return 'none'
|
||||
if (latest.status === 'error') return 'none'
|
||||
return latest.status
|
||||
}
|
||||
|
||||
function errorMsg(format) {
|
||||
const latest = props.latestByFormat[format]
|
||||
if (latest?.status === 'error') return latest.error_msg || 'Fehler bei der Generierung'
|
||||
return ''
|
||||
}
|
||||
|
||||
function handleFormatClick(format) {
|
||||
const guide = props.guidesByFormat[format]
|
||||
if (guide?.status === 'done') {
|
||||
const guide = props.doneByFormat[format]
|
||||
if (guide) {
|
||||
emit('preview', guide)
|
||||
}
|
||||
}
|
||||
@@ -60,16 +70,33 @@ function handlePlay(format) {
|
||||
}
|
||||
|
||||
function handleRefresh(format) {
|
||||
const guide = props.guidesByFormat[format]
|
||||
const guide = props.doneByFormat[format]
|
||||
if (!guide) return
|
||||
const text = activeInput.value === format ? inputText.value.trim() : ''
|
||||
emit('rework', { guideId: guide.id, instructions: text || 'Überarbeite das Layout' })
|
||||
if (!text) return
|
||||
emit('rework', { guideId: guide.id, instructions: text })
|
||||
activeInput.value = null
|
||||
inputText.value = ''
|
||||
}
|
||||
|
||||
function handleInputEnter(format) {
|
||||
const text = inputText.value.trim()
|
||||
if (props.doneByFormat[format] && text) {
|
||||
handleRefresh(format)
|
||||
} else {
|
||||
handlePlay(format)
|
||||
}
|
||||
}
|
||||
|
||||
function dismissError(format) {
|
||||
const latest = props.latestByFormat[format]
|
||||
if (latest?.status === 'error') {
|
||||
emit('deleteGuide', latest.id)
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete(format) {
|
||||
const guide = props.guidesByFormat[format]
|
||||
const guide = props.latestByFormat[format]
|
||||
if (!guide) return
|
||||
if (guide.status === 'generating' || guide.status === 'queued') {
|
||||
if (!confirm('Generierung abbrechen?')) return
|
||||
@@ -91,8 +118,13 @@ function submit() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="sidebar">
|
||||
<aside class="sidebar" @mouseleave="emit('sidebarLeave')">
|
||||
<div class="new-topic">
|
||||
<button
|
||||
class="pin-btn"
|
||||
:title="pinned ? 'Sidebar ausblenden' : 'Sidebar fixieren'"
|
||||
@click="emit('togglePin')"
|
||||
>{{ pinned ? '⇤' : '⇥' }}</button>
|
||||
<input
|
||||
v-model="newTopic"
|
||||
placeholder="Neues Thema…"
|
||||
@@ -128,16 +160,7 @@ function submit() {
|
||||
>×</span>
|
||||
</button>
|
||||
<div class="format-actions">
|
||||
<template v-if="guideStatus(f.key) === 'done'">
|
||||
<button class="action-btn refresh" title="Überarbeiten" @click="handleRefresh(f.key)">↻</button>
|
||||
<button
|
||||
class="action-btn pencil"
|
||||
:class="{ active: activeInput === f.key }"
|
||||
title="Anweisungen"
|
||||
@click="toggleInput(f.key)"
|
||||
>✎</button>
|
||||
</template>
|
||||
<template v-else-if="guideStatus(f.key) !== 'generating' && guideStatus(f.key) !== 'queued'">
|
||||
<template v-if="guideStatus(f.key) !== 'generating' && guideStatus(f.key) !== 'queued'">
|
||||
<button class="action-btn play" title="Generieren" @click="handlePlay(f.key)">▶</button>
|
||||
<button
|
||||
class="action-btn pencil"
|
||||
@@ -148,12 +171,23 @@ function submit() {
|
||||
</template>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
<div v-if="activeInput === f.key" class="format-input">
|
||||
<input
|
||||
v-model="inputText"
|
||||
:placeholder="guideStatus(f.key) === 'done' ? 'Was soll überarbeitet werden?' : 'Anweisungen (optional)…'"
|
||||
@keyup.enter="guideStatus(f.key) === 'done' ? handleRefresh(f.key) : handlePlay(f.key)"
|
||||
:placeholder="doneByFormat[f.key] ? 'Was soll überarbeitet werden?' : 'Anweisungen (optional)…'"
|
||||
@keyup.enter="handleInputEnter(f.key)"
|
||||
/>
|
||||
<button
|
||||
v-if="doneByFormat[f.key]"
|
||||
class="action-btn refresh"
|
||||
title="Überarbeiten"
|
||||
:disabled="!inputText.trim()"
|
||||
@click="handleRefresh(f.key)"
|
||||
>↻</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bausteine-btn-wrapper">
|
||||
@@ -214,6 +248,20 @@ function submit() {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.new-topic .pin-btn {
|
||||
background: #f8f9fb;
|
||||
color: #4b5563;
|
||||
border: 1px solid #d8dde3;
|
||||
font-weight: 600;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.new-topic .pin-btn:hover {
|
||||
background: #ede9fe;
|
||||
color: #4f46e5;
|
||||
border-color: #a5b4fc;
|
||||
}
|
||||
|
||||
.topic-list {
|
||||
list-style: none;
|
||||
overflow-y: auto;
|
||||
@@ -332,10 +380,35 @@ function submit() {
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.fmt-error .format-name {
|
||||
.format-error {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
padding: 2px 0.75rem 6px calc(0.75rem + 8px);
|
||||
font-size: 0.72rem;
|
||||
color: #991b1b;
|
||||
background: #fee2e2;
|
||||
border: 1px solid #f87171;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.format-error-text {
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.format-error-x {
|
||||
flex: 0 0 auto;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #991b1b;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
padding: 0 2px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.format-error-x:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.format-actions {
|
||||
@@ -387,11 +460,14 @@ function submit() {
|
||||
}
|
||||
|
||||
.format-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 0.75rem 8px;
|
||||
}
|
||||
|
||||
.format-input input {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #d8dde3;
|
||||
border-radius: 4px;
|
||||
@@ -403,6 +479,16 @@ function submit() {
|
||||
border-color: #6366f1;
|
||||
}
|
||||
|
||||
.action-btn:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-btn:disabled:hover {
|
||||
background: none;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.65; }
|
||||
|
||||
Reference in New Issue
Block a user