This commit is contained in:
team3
2026-07-02 22:48:57 +02:00
parent 41c9f29a37
commit 285317927d
38 changed files with 2548 additions and 2812 deletions

View File

@@ -1,13 +1,11 @@
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import { fetchGuides, fetchTopics, deleteTopic as apiDeleteTopic, createGuide as apiCreate, deleteGuide, cancelGuide as apiCancel, fetchBlocksStatus, fetchActiveBlocks, createBlocks as apiCreateBausteine, resetBlocksStage as apiResetBlocksStage, addBlocksResearch as apiAddResearch, requeueBlocksDead as apiRequeueDead, resetGuideBoard as apiResetGuideBoard, cancelBlocks as apiCancelBausteine, deleteBlocks as apiDeleteBausteine, fetchProviders, fetchStats, fetchTopicProgress, fetchGuideLocks, fetchGuideSteps, fetchFolders, updateSource as apiUpdateQuelle } from './api.js'
import { fetchGuides, fetchTopics, deleteTopic as apiDeleteTopic, createGuide as apiCreate, deleteGuide, cancelGuide as apiCancel, fetchBlocksStatus, fetchActiveBlocks, createBlocks as apiCreateBausteine, resetBlocksStage as apiResetBlocksStage, addBlocksResearch as apiAddResearch, requeueBlocksDead as apiRequeueDead, resetGuideBoard as apiResetGuideBoard, restartBlocksCard as apiRestartBlocksCard, resetGuideCard as apiResetGuideCard, cancelBlocks as apiCancelBausteine, deleteBlocks as apiDeleteBausteine, fetchProviders, fetchStats, fetchTopicProgress, fetchGuideLocks, fetchFolders, updateSource as apiUpdateQuelle } from './api.js'
import { usePolling } from './composables/usePolling.js'
import TopicSidebar from './components/TopicSidebar.vue'
import TopicDetail from './components/TopicDetail.vue'
import BlocksOverview from './components/BlocksOverview.vue'
import GuideBoard from './components/GuideBoard.vue'
import ElementsSidebar from './components/elements/ElementsSidebar.vue'
import ElementsOverview from './components/ElementsOverview.vue'
import GenerationView from './components/GenerationView.vue'
import GeneralExamPanel from './components/GeneralExamPanel.vue'
const guides = ref([])
@@ -28,19 +26,14 @@ const activeBlocks = ref([])
const provider = ref(localStorage.getItem('provider') || 'claude')
const providers = ref([])
const folders = ref({ projekt: [], uni: [] }) // folders for the sources picker
const mainView = ref('blocks') // blocks | guideboard | elements | general | detail — exclusive main-area view
const mainView = ref('blocks') // blocks | generation | general | detail — exclusive main-area view
const guideBoardFormat = ref('Guide')
const viewMode = ref('compact') // compact | erklärend — per topic, default compact
const levelView = ref(Number(localStorage.getItem('level')) || 4) // 1=A · 2=F · 3=E · 4=V (levels view)
const stats = ref(null)
const progress = ref({})
const locks = ref({}) // lock reasons per format (backend = single rule source)
const guideStepsDone = ref({}) // highest finished step index per format (artifact-based)
const uiError = ref(null) // surface rejected actions (409/400)
const elementsOpen = ref(false) // right sidebar
const elementsVersion = ref(0) // increment = reload overview
const elementOpenId = ref(null) // open element from overview in sidebar
const elementOpenTick = ref(0)
// Run a loader, log + swallow its error (a failed background load must not break the UI).
async function guard(label, fn) {
@@ -172,12 +165,10 @@ async function loadBlocks() {
blocks.value = await fetchBlocksStatus(selectedTopic.value)
progress.value = await fetchTopicProgress(selectedTopic.value)
locks.value = await fetchGuideLocks(selectedTopic.value)
guideStepsDone.value = await fetchGuideSteps(selectedTopic.value)
} else {
blocks.value = { ...EMPTY_BLOCKS }
progress.value = {}
locks.value = {}
guideStepsDone.value = {}
}
if (activeBlocks.value.length && !polling.running()) startPolling()
} catch (e) {
@@ -189,9 +180,7 @@ function selectTopic(topic) {
selectedTopic.value = topic
previewGuide.value = null
sidebarSticky.value = false
elementsOpen.value = false
mainView.value = 'blocks' // topic click → blocks overview (guide only on pill click)
elementOpenId.value = null
viewMode.value = localStorage.getItem('ansicht_' + topic) === 'erklärend' ? 'erklärend' : 'compact'
localStorage.setItem('lastTopic', topic)
loadBlocks()
@@ -273,6 +262,7 @@ async function handleCreateTopic({ topic, instructions, sourceType, sourceOrt })
}
await loadTopics()
selectTopic(topic)
mainView.value = 'generation' // frisches Topic: die Boards sind das Einzige, was passiert
startPolling()
}
@@ -326,10 +316,35 @@ async function handleFormatClick({ format, instructions = '', abStep = null }) {
function handleOpenGuideBoard(format = 'Guide') {
if (!selectedTopic.value) return
guideBoardFormat.value = format
mainView.value = 'guideboard'
mainView.value = 'generation'
previewGuide.value = null
}
function handleOpenGeneration() {
if (!selectedTopic.value) return
mainView.value = 'generation'
previewGuide.value = null
}
async function handleRestartCard(cardId) {
uiError.value = null
try {
await apiRestartBlocksCard(selectedTopic.value, cardId)
await handleBlocksClick({ research: false }) // Continue: der Flow zieht die Karte
} catch (e) {
uiError.value = e.message
}
}
async function handleResetGuideCard({ format, blockNorm, abStage }) {
uiError.value = null
try {
await apiResetGuideCard(selectedTopic.value, format, blockNorm, abStage)
} catch (e) {
uiError.value = e.message
}
}
async function handleGuideBoardReset({ format, abStage }) {
uiError.value = null
try {
@@ -355,18 +370,6 @@ function handleGeneralExam() {
previewGuide.value = null
}
function handleOpenElements() {
if (!selectedTopic.value) return
mainView.value = 'elements'
// Right sidebar stays closed — it opens only when an element is clicked.
}
function handleOpenElementDetail(el) {
elementOpenId.value = el.id
elementOpenTick.value++
elementsOpen.value = true
}
async function handleDeleteGuide(guideId, slots = false) {
await deleteGuide(guideId, slots)
if (previewGuide.value?.id === guideId) {
@@ -422,7 +425,6 @@ onMounted(async () => {
:stats="stats"
:fortschritt="progress"
:locks="locks"
:guideStepsDone="guideStepsDone"
:uiError="uiError"
:doneByFormat="doneByFormat"
:latestByFormat="latestByFormat"
@@ -447,17 +449,12 @@ onMounted(async () => {
@updateSource="handleUpdateSource"
@openBausteineView="handleOpenBlocksView"
@openGuideBoard="handleOpenGuideBoard"
@formatClick="handleFormatClick"
@bausteineClick="handleBlocksClick"
@cancelBlocks="handleCancelBlocks"
@resetBausteine="handleResetBlocks"
@deleteTopic="handleDeleteTopic"
@cancelGuide="handleCancel"
@deleteGuide="handleDeleteGuide"
@dismissError="handleDismissError"
@dismissUiError="uiError = null"
@preview="handlePreview"
@openElements="handleOpenElements"
@openGeneration="handleOpenGeneration"
@togglePin="toggleSidebarPin"
@sidebarLeave="onSidebarLeave"
/>
@@ -469,6 +466,17 @@ onMounted(async () => {
:ready="blocks.ready"
:partial="blocks.partial"
@close="mainView = 'detail'"
@openGeneration="handleOpenGeneration"
/>
<GenerationView
v-else-if="selectedTopic && mainView === 'generation'"
:topic="selectedTopic"
:generating="blocks.generating"
:progress="blocks.progress"
:ready="blocks.ready"
:partial="blocks.partial"
:guideFormat="guideBoardFormat"
@close="mainView = 'blocks'"
@resetStage="handleResetStage"
@restartAll="() => handleBlocksClick({ research: true })"
@continueAll="() => handleBlocksClick({ research: false })"
@@ -476,22 +484,13 @@ onMounted(async () => {
@requeueDead="handleRequeueDead"
@removeAll="handleResetBlocks"
@cancel="handleCancelBlocks"
/>
<GuideBoard
v-else-if="selectedTopic && mainView === 'guideboard'"
:topic="selectedTopic"
:format="guideBoardFormat"
@close="mainView = 'blocks'"
@cancelGuide="handleCancel"
@startGuide="handleFormatClick"
@resetStage="handleGuideBoardReset"
@resetGuideStage="handleGuideBoardReset"
@preview="handleGuideBoardPreview"
/>
<ElementsOverview
v-else-if="selectedTopic && mainView === 'elements'"
:topic="selectedTopic"
:version="elementsVersion"
@open="handleOpenElementDetail"
@deleteGuide="handleDeleteGuide"
@restartCard="handleRestartCard"
@resetGuideCard="handleResetGuideCard"
/>
<GeneralExamPanel
v-else-if="selectedTopic && mainView === 'general'"
@@ -505,7 +504,6 @@ onMounted(async () => {
:previewGuide="previewGuide"
:dark="darkMode"
:provider="provider"
:elementsOpen="elementsOpen"
:doneByFormat="doneByFormat"
:themaAbgeschlossen="!!progress.completed"
:ansichtModus="viewMode"
@@ -518,20 +516,6 @@ onMounted(async () => {
<div v-else class="empty-main">
<p>Create or select a topic in the sidebar.</p>
</div>
<div
v-if="elementsOpen && selectedTopic"
class="elements-backdrop"
@click="elementsOpen = false"
></div>
<ElementsSidebar
v-if="elementsOpen && selectedTopic"
:topic="selectedTopic"
:provider="provider"
:openId="elementOpenId"
:openTick="elementOpenTick"
@close="elementsOpen = false"
@changed="elementsVersion++"
/>
</div>
</template>
@@ -685,19 +669,4 @@ textarea::placeholder {
font-size: 1rem;
}
/* Only visible when the elements sidebar sits as an overlay on mobile.
A tap next to it closes it. */
.elements-backdrop {
display: none;
}
@media (max-width: 768px) {
.elements-backdrop {
display: block;
position: fixed;
inset: 0;
z-index: 29;
background: var(--shadow);
}
}
</style>