This commit is contained in:
team3
2026-05-28 18:10:58 +02:00
parent cc1ea166c8
commit c1370a9f6a
2 changed files with 179 additions and 32 deletions

View File

@@ -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 {

View File

@@ -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() {
>&times;</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)">&times;</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; }