update
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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">×</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">×</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;
|
||||
|
||||
Reference in New Issue
Block a user