This commit is contained in:
team3
2026-06-03 22:05:20 +02:00
parent d7e8df6876
commit 40de56c27b
5119 changed files with 552560 additions and 24 deletions

View File

@@ -1,12 +1,13 @@
<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { fetchGuides, createGuide as apiCreate, deleteGuide, cancelGuide as apiCancel, reworkGuide as apiRework, createBaustein as apiCreateBaustein } from './api.js'
import { fetchGuides, createGuide as apiCreate, deleteGuide, cancelGuide as apiCancel, reworkGuide as apiRework, createBaustein as apiCreateBaustein, fetchProjects, deleteProject as apiDeleteProject } from './api.js'
import TopicSidebar from './components/TopicSidebar.vue'
import TopicDetail from './components/TopicDetail.vue'
import BausteineView from './components/BausteineView.vue'
import HelpChat from './components/HelpChat.vue'
const guides = ref([])
const projects = ref([])
const manualTopics = ref([])
const selectedTopic = ref(null)
const previewGuide = ref(null)
@@ -31,19 +32,26 @@ function onSidebarLeave() {
if (!sidebarPinned.value) sidebarSticky.value = false
}
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 manualTopics.value) {
if (isProject.has(t)) continue
if (!topicDates[t]) topicDates[t] = new Date().toISOString()
}
return Object.keys(topicDates).sort((a, b) => topicDates[b].localeCompare(topicDates[a]))
})
const isProjectSelected = computed(() => projectNames.value.includes(selectedTopic.value))
const doneByFormat = computed(() => {
const map = {}
for (const g of guides.value) {
@@ -79,6 +87,14 @@ async function loadGuides() {
}
}
async function loadProjects() {
try {
projects.value = await fetchProjects()
} catch (e) {
console.error('Fehler beim Laden der Projekte:', e)
}
}
const FORMAT_ORDER = ['OnePager', 'Cheatsheet', 'MiniGuide', 'Guide', 'EndGuide']
function autoPreview() {
@@ -113,13 +129,22 @@ function onHelpSelect(title) {
createTopic(title)
}
async function handleFormatClick({ format, instructions }) {
async function handleFormatClick({ format, instructions, reindex }) {
if (!selectedTopic.value) return
await apiCreate(selectedTopic.value, format, instructions)
await apiCreate(selectedTopic.value, format, instructions, reindex || false)
await loadGuides()
startPolling()
}
async function handleDeleteProject(name) {
await apiDeleteProject(name)
if (selectedTopic.value === name) {
selectedTopic.value = null
previewGuide.value = null
}
await loadProjects()
}
async function handleRework({ guideId, instructions }) {
await apiRework(guideId, instructions)
await loadGuides()
@@ -193,7 +218,7 @@ function onVisibility() {
}
onMounted(async () => {
await loadGuides()
await Promise.all([loadGuides(), loadProjects()])
if (!selectedTopic.value && topics.value.length) {
selectTopic(topics.value[0])
}
@@ -211,6 +236,8 @@ onUnmounted(() => {
<div v-if="!sidebarPinned" class="hover-zone" @click="clickHoverZone"></div>
<TopicSidebar
:topics="topics"
:projects="projectNames"
:isProjectSelected="isProjectSelected"
:selectedTopic="selectedTopic"
:doneByFormat="doneByFormat"
:latestByFormat="latestByFormat"
@@ -221,6 +248,7 @@ onUnmounted(() => {
@create="createTopic"
@formatClick="handleFormatClick"
@deleteTopic="handleDeleteTopic"
@deleteProject="handleDeleteProject"
@cancelGuide="handleCancel"
@deleteGuide="handleDeleteGuide"
@preview="handlePreview"

View File

@@ -10,15 +10,24 @@ export async function fetchGuide(id) {
return res.json()
}
export async function createGuide(topic, format, instructions = '') {
export async function createGuide(topic, format, instructions = '', reindex = false) {
const res = await fetch(`${BASE}/guides`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic, format, instructions }),
body: JSON.stringify({ topic, format, instructions, reindex }),
})
return res.json()
}
export async function fetchProjects() {
const res = await fetch(`${BASE}/projects`)
return res.json()
}
export async function deleteProject(name) {
await fetch(`${BASE}/projects/${encodeURIComponent(name)}`, { method: 'DELETE' })
}
export async function reworkGuide(id, instructions) {
const res = await fetch(`${BASE}/guides/${id}/rework`, {
method: 'POST',

View File

@@ -3,6 +3,8 @@ import { ref, computed } from 'vue'
const props = defineProps({
topics: { type: Array, required: true },
projects: { type: Array, default: () => [] },
isProjectSelected: { type: Boolean, default: false },
selectedTopic: { type: String, default: null },
doneByFormat: { type: Object, default: () => ({}) },
latestByFormat: { type: Object, default: () => ({}) },
@@ -11,7 +13,9 @@ const props = defineProps({
pinned: { type: Boolean, default: true },
})
const emit = defineEmits(['select', 'create', 'formatClick', 'deleteTopic', 'cancelGuide', 'deleteGuide', 'preview', 'rework', 'showBausteine', 'addBaustein', 'togglePin', 'sidebarLeave', 'openHelp'])
const emit = defineEmits(['select', 'create', 'formatClick', 'deleteTopic', 'deleteProject', 'cancelGuide', 'deleteGuide', 'preview', 'rework', 'showBausteine', 'addBaustein', 'togglePin', 'sidebarLeave', 'openHelp'])
const reindex = ref(false)
const quickBausteinTitle = ref('')
@@ -72,9 +76,10 @@ function toggleInput(format) {
function handlePlay(format) {
const text = activeInput.value === format ? inputText.value.trim() : ''
emit('formatClick', { format, instructions: text })
emit('formatClick', { format, instructions: text, reindex: props.isProjectSelected && reindex.value })
activeInput.value = null
inputText.value = ''
reindex.value = false
}
function handleRefresh(format) {
@@ -128,6 +133,11 @@ function confirmDeleteTopic(topic) {
if (!confirm(`Thema "${topic}" und alle zugehörigen Guides löschen?`)) return
emit('deleteTopic', topic)
}
function confirmDeleteProject(name) {
if (!confirm(`Projekt "${name}" entfernen?\n\nAchtung: Der Quellordner ./projects/${name} und der Cache werden gelöscht.`)) return
emit('deleteProject', name)
}
</script>
<template>
@@ -154,6 +164,10 @@ function confirmDeleteTopic(topic) {
<div class="progress-info" v-if="activeGenerations.length">
<div v-for="(line, i) in activeGenerations" :key="i">{{ line }}</div>
</div>
<label v-if="isProjectSelected" class="reindex-toggle">
<input type="checkbox" v-model="reindex" />
<span>Projekt neu einlesen</span>
</label>
<div v-for="f in formats" :key="f.key">
<div :class="['format-row', 'fmt-' + guideStatus(f.key)]">
<button class="format-name" @click="handleFormatClick(f.key)">
@@ -223,6 +237,18 @@ function confirmDeleteTopic(topic) {
<span>{{ t }}</span>
<button class="delete-topic" @click.stop="confirmDeleteTopic(t)" title="Löschen">&times;</button>
</li>
<template v-if="projects.length">
<li class="projects-divider">Projekte</li>
<li
v-for="p in projects"
:key="'project-' + p"
:class="{ active: p === selectedTopic, 'project-item': true }"
@click="emit('select', p)"
>
<span>{{ p }}</span>
<button class="delete-topic" @click.stop="confirmDeleteProject(p)" title="Projekt entfernen">&times;</button>
</li>
</template>
</ul>
</aside>
</template>
@@ -348,6 +374,40 @@ function confirmDeleteTopic(topic) {
display: block;
}
.projects-divider {
cursor: default;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #9ca3af;
font-weight: 700;
padding: 0.6rem 1rem 0.3rem;
margin-top: 0.4rem;
border-top: 1px solid #e2e5e9;
}
.projects-divider:hover {
background: none;
}
.topic-list li.project-item span::before {
content: '📁 ';
}
.reindex-toggle {
display: flex;
align-items: center;
gap: 6px;
padding: 0.4rem 0.75rem;
font-size: 0.8rem;
color: #4b5563;
cursor: pointer;
}
.reindex-toggle input {
cursor: pointer;
}
/* Format section */
.format-section {
flex-shrink: 0;