update
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<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, restartBlocksCard as apiRestartBlocksCard, resetGuideCard as apiResetGuideCard, removeGuideFormat as apiRemoveGuideFormat, cancelBlocks as apiCancelBausteine, deleteBlocks as apiDeleteBausteine, fetchProviders, fetchStats, fetchTopicProgress, 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, addBlocksResearch as apiAddResearch, requeueBlocksDead as apiRequeueDead, removeGuideFormat as apiRemoveGuideFormat, cancelBlocks as apiCancelBausteine, deleteBlocks as apiDeleteBausteine, fetchProviders, fetchStats, fetchTopicProgress, 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'
|
||||
@@ -182,12 +182,31 @@ async function loadBlocks() {
|
||||
}
|
||||
}
|
||||
|
||||
// „Auto"-Ebenen-Flags pro Topic (localStorage): nach der QA „Befunde beheben" bis 100 %.
|
||||
const autoInventory = ref(true)
|
||||
const autoArtefacts = ref(true)
|
||||
const autoGuide = ref(true)
|
||||
function ladeAuto(topic) {
|
||||
const g = (e) => { const v = localStorage.getItem(`auto_${topic}_${e}`); return v === null ? true : v === 'true' }
|
||||
autoInventory.value = g('inventory')
|
||||
autoArtefacts.value = g('artefacts')
|
||||
autoGuide.value = g('guide')
|
||||
}
|
||||
function handleSetAuto({ ebene, value }) {
|
||||
const r = { inventory: autoInventory, artefacts: autoArtefacts, guide: autoGuide }[ebene]
|
||||
if (!r) return
|
||||
r.value = value
|
||||
if (selectedTopic.value) localStorage.setItem(`auto_${selectedTopic.value}_${ebene}`, value ? 'true' : 'false')
|
||||
}
|
||||
const autoObj = () => ({ inventory: autoInventory.value, artefacts: autoArtefacts.value, guide: autoGuide.value })
|
||||
|
||||
function selectTopic(topic) {
|
||||
selectedTopic.value = topic
|
||||
previewGuide.value = null
|
||||
sidebarSticky.value = false
|
||||
mainView.value = 'generation' // topic click → generation board (guide only on pill click)
|
||||
viewMode.value = localStorage.getItem('ansicht_' + topic) === 'erklärend' ? 'erklärend' : 'compact'
|
||||
ladeAuto(topic)
|
||||
localStorage.setItem('lastTopic', topic)
|
||||
loadBlocks()
|
||||
}
|
||||
@@ -207,20 +226,6 @@ async function handleResetBlocks() {
|
||||
if (await withUiError(() => apiDeleteBausteine(selectedTopic.value))) await loadBlocks()
|
||||
}
|
||||
|
||||
async function handleResetStage({ board, stage, restart = false }) {
|
||||
if (!selectedTopic.value) return
|
||||
uiError.value = null
|
||||
try {
|
||||
await apiResetBlocksStage(selectedTopic.value, board, stage)
|
||||
if (restart) await apiCreateBausteine(selectedTopic.value, '', provider.value, undefined, undefined, false) // Continue: Queue abarbeiten
|
||||
} catch (e) {
|
||||
uiError.value = e.message
|
||||
return
|
||||
}
|
||||
await loadBlocks()
|
||||
if (restart) startPolling()
|
||||
}
|
||||
|
||||
async function handleAddResearch() {
|
||||
if (!selectedTopic.value) return
|
||||
uiError.value = null
|
||||
@@ -238,7 +243,7 @@ async function handleRequeueDead() {
|
||||
if (!selectedTopic.value) return
|
||||
const ok = await withUiError(async () => {
|
||||
await apiRequeueDead(selectedTopic.value)
|
||||
await apiCreateBausteine(selectedTopic.value, '', provider.value, undefined, undefined, false)
|
||||
await apiCreateBausteine(selectedTopic.value, '', provider.value, undefined, undefined, false, false, autoObj())
|
||||
})
|
||||
if (!ok) return
|
||||
await loadBlocks()
|
||||
@@ -250,7 +255,7 @@ async function handleBlocksClick({ instructions = '', research = true, qaForce =
|
||||
uiError.value = null
|
||||
try {
|
||||
// research=true = Start/mehr Research anhängen; false = Continue (Queue abarbeiten).
|
||||
await apiCreateBausteine(selectedTopic.value, instructions, provider.value, undefined, undefined, research, qaForce)
|
||||
await apiCreateBausteine(selectedTopic.value, instructions, provider.value, undefined, undefined, research, qaForce, autoObj())
|
||||
} catch (e) {
|
||||
uiError.value = e.message
|
||||
return
|
||||
@@ -300,7 +305,7 @@ function handleOpenBlocksView() {
|
||||
previewGuide.value = null
|
||||
}
|
||||
|
||||
async function handleFormatClick({ format, instructions = '', abStep = null }) {
|
||||
async function handleFormatClick({ format, instructions = '' }) {
|
||||
if (!selectedTopic.value) return
|
||||
// No duplicate start: if a generation is already running for topic+format, ignore
|
||||
const running = guides.value.some(
|
||||
@@ -310,7 +315,7 @@ async function handleFormatClick({ format, instructions = '', abStep = null }) {
|
||||
if (running) { handleOpenGuideBoard(format); return }
|
||||
uiError.value = null
|
||||
try {
|
||||
await apiCreate(selectedTopic.value, format, instructions, provider.value, abStep)
|
||||
await apiCreate(selectedTopic.value, format, instructions, provider.value)
|
||||
} catch (e) {
|
||||
uiError.value = e.message
|
||||
return
|
||||
@@ -344,34 +349,6 @@ async function handleRemoveGuideFormat(format) {
|
||||
await loadGuides()
|
||||
}
|
||||
|
||||
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 {
|
||||
await apiResetGuideBoard(selectedTopic.value, format, abStage)
|
||||
} catch (e) {
|
||||
uiError.value = e.message
|
||||
}
|
||||
}
|
||||
|
||||
function handleGuideBoardPreview() {
|
||||
const g = doneByFormat.value[guideBoardFormat.value]
|
||||
if (g) handlePreview(g)
|
||||
@@ -502,8 +479,10 @@ onMounted(async () => {
|
||||
:partial="blocks.partial"
|
||||
:error="blocks.error"
|
||||
:guideFormat="guideBoardFormat"
|
||||
:autoInventory="autoInventory"
|
||||
:autoArtefacts="autoArtefacts"
|
||||
:autoGuide="autoGuide"
|
||||
@close="mainView = 'blocks'"
|
||||
@resetStage="handleResetStage"
|
||||
@restartAll="() => handleBlocksClick({ research: true })"
|
||||
@continueAll="(opts) => handleBlocksClick({ research: false, qaForce: !!(opts && opts.qaForce) })"
|
||||
@addResearch="handleAddResearch"
|
||||
@@ -512,11 +491,9 @@ onMounted(async () => {
|
||||
@cancel="handleCancelBlocks"
|
||||
@cancelGuide="handleCancel"
|
||||
@startGuide="handleFormatClick"
|
||||
@resetGuideStage="handleGuideBoardReset"
|
||||
@preview="handleGuideBoardPreview"
|
||||
@removeFormat="handleRemoveGuideFormat"
|
||||
@restartCard="handleRestartCard"
|
||||
@resetGuideCard="handleResetGuideCard"
|
||||
@setAuto="handleSetAuto"
|
||||
/>
|
||||
<GeneralExamPanel
|
||||
v-else-if="selectedTopic && mainView === 'general'"
|
||||
|
||||
@@ -35,15 +35,18 @@ async function req(path, { method = 'GET', body, query } = {}) {
|
||||
|
||||
export const fetchGuides = () => req('/guides')
|
||||
|
||||
export const createGuide = (topic, format, instructions = '', provider = 'claude', abStep = null) =>
|
||||
req('/guides', { method: 'POST', body: { topic, format, instructions, provider, ab_step: abStep } })
|
||||
export const createGuide = (topic, format, instructions = '', provider = 'claude') =>
|
||||
req('/guides', { method: 'POST', body: { topic, format, instructions, provider } })
|
||||
|
||||
export const fetchActiveBlocks = () => req('/blocks/active')
|
||||
|
||||
export const fetchBlocksStatus = (topic) => req('/blocks/status', { query: { topic } })
|
||||
|
||||
export const createBlocks = (topic, instructions = '', provider = 'claude', sourceType = 'thema', sourceOrt = '', research = true, qaForce = false) =>
|
||||
req('/blocks', { method: 'POST', body: { topic, instructions, provider, source_type: sourceType, source_location: sourceOrt, research, qa_force: qaForce } })
|
||||
export const createBlocks = (topic, instructions = '', provider = 'claude', sourceType = 'thema', sourceOrt = '', research = true, qaForce = false, auto = {}) =>
|
||||
req('/blocks', { method: 'POST', body: {
|
||||
topic, instructions, provider, source_type: sourceType, source_location: sourceOrt, research, qa_force: qaForce,
|
||||
auto_inventory: auto.inventory ?? true, auto_artefacts: auto.artefacts ?? true, auto_guide: auto.guide ?? true,
|
||||
} })
|
||||
|
||||
// Live-Kanban-Board der Blocks-Erzeugung (Spalten + Karten + Agenten + Dead-Letter).
|
||||
export const fetchBlocksBoard = (topic) => req('/blocks/board', { query: { topic } })
|
||||
@@ -51,26 +54,19 @@ export const fetchBlocksBoard = (topic) => req('/blocks/board', { query: { topic
|
||||
// Manueller QA-Lauf (wie das Gate, inkl. LLM-Stichprobe); Badge liest den neuen Report.
|
||||
export const runQa = (topic, llm = true) => req('/blocks/qa', { method: 'POST', body: { topic, llm } })
|
||||
|
||||
// QA-Befunde gezielt beheben (Hygiene, bestätigte Dubletten, Fremd/Unecht nach Gegen-Judge).
|
||||
export const runRepair = (topic) => req('/blocks/repair', { method: 'POST', body: { topic } })
|
||||
// QA-Befunde gezielt beheben; ebene = 'inventory' | 'artefacts' | undefined (alle).
|
||||
export const runRepair = (topic, ebene = null) => req('/blocks/repair', { method: 'POST', body: { topic, ebene } })
|
||||
|
||||
// Karten ab Spalte zurücksetzen (keine Generierung).
|
||||
export const resetBlocksStage = (topic, board, stage) =>
|
||||
req('/blocks/reset-stage', { method: 'POST', body: { topic, board, stage } })
|
||||
// Artefakte-Ebene leeren (Board-2-Daten weg, Inventar bleibt).
|
||||
export const removeArtefacts = (topic) => req('/blocks/artefacts', { method: 'DELETE', query: { topic } })
|
||||
|
||||
// Einen weiteren Research-Agenten anhängen (Attach-or-Start).
|
||||
export const addBlocksResearch = (topic, provider = 'claude') =>
|
||||
req('/blocks/research', { method: 'POST', query: { topic, provider } })
|
||||
|
||||
export const restartBlocksCard = (topic, cardId) =>
|
||||
req('/blocks/card-restart', { method: 'POST', body: { topic, card_id: cardId } })
|
||||
|
||||
export const removeGuideFormat = (topic, format) =>
|
||||
req('/guides/board/remove', { method: 'POST', body: { topic, format } })
|
||||
|
||||
export const resetGuideCard = (topic, format, blockNorm, abStage) =>
|
||||
req('/guides/board/card-reset', { method: 'POST', body: { topic, format, block_norm: blockNorm, ab_stage: abStage } })
|
||||
|
||||
export const requeueBlocksDead = (topic) =>
|
||||
req('/blocks/requeue-dead', { method: 'POST', query: { topic } })
|
||||
|
||||
@@ -78,12 +74,9 @@ export const requeueBlocksDead = (topic) =>
|
||||
export const fetchGuideBoard = (topic, format = 'Guide') =>
|
||||
req('/guides/board', { query: { topic, format } })
|
||||
|
||||
export const resetGuideBoard = (topic, format, abStage) =>
|
||||
req('/guides/board/reset', { method: 'POST', body: { topic, format, ab_stage: abStage } })
|
||||
|
||||
// Befunde beheben: Karten mit QA-Befunden zurück auf Prüfen + Resume-Lauf.
|
||||
export const repairGuideBoard = (topic, format) =>
|
||||
req('/guides/board/repair', { method: 'POST', body: { topic, format, ab_stage: 0 } })
|
||||
req('/guides/board/repair', { method: 'POST', body: { topic, format } })
|
||||
|
||||
export const cancelBlocks = (topic) => req('/blocks/cancel', { method: 'POST', query: { topic } })
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, onUnmounted } from 'vue'
|
||||
import { fetchBlocksBoard, fetchRuns, runQa, runRepair } from '../api.js'
|
||||
import { fetchBlocksBoard, fetchRuns, runQa, runRepair, removeArtefacts } from '../api.js'
|
||||
import { usePolling } from '../composables/usePolling.js'
|
||||
import { useConfirm } from '../composables/useConfirm.js'
|
||||
import { fmtRuntime, fmtTokens } from '../format.js'
|
||||
@@ -16,9 +16,12 @@ const props = defineProps({
|
||||
partial: { type: Boolean, default: false },
|
||||
error: { type: String, default: null },
|
||||
guideFormat: { type: String, default: 'Guide' },
|
||||
autoInventory: { type: Boolean, default: true },
|
||||
autoArtefacts: { type: Boolean, default: true },
|
||||
autoGuide: { type: Boolean, default: true },
|
||||
})
|
||||
const emit = defineEmits(['close', 'resetStage', 'restartAll', 'continueAll', 'addResearch',
|
||||
'requeueDead', 'removeAll', 'cancel', 'cancelGuide', 'startGuide', 'resetGuideStage', 'preview', 'removeFormat', 'restartCard', 'resetGuideCard'])
|
||||
const emit = defineEmits(['close', 'restartAll', 'continueAll', 'addResearch', 'requeueDead',
|
||||
'removeAll', 'removeArtefacts', 'cancel', 'cancelGuide', 'startGuide', 'preview', 'removeFormat', 'setAuto'])
|
||||
|
||||
// ── Blocks-Pipeline (Poll 1.2s solange generiert; Visibility-Pause via usePolling) ──
|
||||
const board = ref(null)
|
||||
@@ -29,8 +32,6 @@ async function pollBoard() {
|
||||
board.value = await fetchBlocksBoard(props.topic)
|
||||
pollError.value = null
|
||||
} catch (e) {
|
||||
// 404 = Board noch nicht gebaut (kein Fehler); alles andere (500/Netz) sichtbar machen,
|
||||
// statt es als „leeres Board" zu tarnen
|
||||
if (e.status === 404) board.value = null
|
||||
else pollError.value = e.message
|
||||
}
|
||||
@@ -49,8 +50,6 @@ const dead = computed(() => board.value?.dead || [])
|
||||
const qa = computed(() => board.value?.qa || null)
|
||||
|
||||
// Ehrlicher Fortschritt aus den Spaltenzahlen: gewichteter Spaltenindex (Terminal = 1,0).
|
||||
// Kein einzelner „100%"-Wert wird geraten — wächst der Umfang (Research legt Karten nach),
|
||||
// SINKT der Wert; das ist die Wahrheit und wird mit „Umfang wächst noch" gekennzeichnet.
|
||||
const TERMINAL = new Set(['done_block', 'done_artefact', 'rejected', 'grouped'])
|
||||
function boardProgress(cols) {
|
||||
const total = cols.reduce((n, c) => n + c.total, 0)
|
||||
@@ -65,6 +64,9 @@ const inventoryProgress = computed(() => boardProgress(inventoryCols.value))
|
||||
const artefactProgress = computed(() => boardProgress(artefactCols.value))
|
||||
const scopeGrowing = computed(() => props.generating && (board.value?.agents?.length || 0) > 0)
|
||||
|
||||
// Score 0–10 → „100 %"
|
||||
const pct = (note) => Math.round(note * 10) + ' %'
|
||||
|
||||
// ── Laufzeit + Tokens aus /api/runs (5s-Takt, unabhängig vom 1,2s-Board-Poll) ──────
|
||||
const run = ref(null)
|
||||
const now = ref(Date.now())
|
||||
@@ -97,79 +99,40 @@ watch(() => props.generating, (g) => {
|
||||
watch(() => props.topic, () => { run.value = null; loadRun() })
|
||||
onUnmounted(() => { if (clock) clearInterval(clock) })
|
||||
|
||||
// Spalten, auf die zurückgesetzt werden kann (Terminal-Spalten sind kein Reset-Ziel).
|
||||
const RESETTABLE = new Set(['ingest', 'cluster', 'pair_check', 'consensus_gate', 'clarify', 'naming',
|
||||
'naming_check', 'fragment_filter', 'grouping', 'gap_check', 'done',
|
||||
'subblocks', 'facts', 'konsolidierung', 'levels', 'relevance', 'question_pattern', 'artefacts', 'finalize', 'outline'])
|
||||
const sel = ref(null) // gewählte Spalte {board, key, label}
|
||||
const selCard = ref(null) // gewählte Karte (Einzel-Restart, nur artefacts)
|
||||
const { isArmed, armOrRun, reset: resetConfirm } = useConfirm() // 2-Klick-Bestätigung (mit 3s-Auto-Reset)
|
||||
|
||||
function stageClick(c) {
|
||||
if (props.generating || !RESETTABLE.has(c.key)) return
|
||||
resetConfirm()
|
||||
selCard.value = null
|
||||
sel.value = sel.value?.key === c.key ? null : { board: c.board, key: c.key, label: c.label }
|
||||
}
|
||||
|
||||
function cardClick(k) {
|
||||
if (props.generating || k.kind !== 'ablock') return // Einzel-Restart nur für Artefakt-Karten
|
||||
resetConfirm()
|
||||
sel.value = null
|
||||
selCard.value = selCard.value?.card_id === k.card_id ? null : k
|
||||
}
|
||||
|
||||
function restartCard() {
|
||||
const k = selCard.value
|
||||
selCard.value = null
|
||||
resetConfirm()
|
||||
later(() => emit('restartCard', k.card_id))
|
||||
}
|
||||
function later(fn) { // Aktion emitten, Board kurz danach neu laden (kein generating-Poll aktiv)
|
||||
fn()
|
||||
setTimeout(pollBoard, 600)
|
||||
}
|
||||
|
||||
function resetHere(restart) {
|
||||
const s = sel.value
|
||||
sel.value = null
|
||||
resetConfirm()
|
||||
later(() => emit('resetStage', { board: s.board, stage: s.key, restart }))
|
||||
}
|
||||
const { isArmed, armOrRun, reset: resetConfirm } = useConfirm() // 2-Klick-Bestätigung
|
||||
function later(fn) { fn(); setTimeout(pollBoard, 600) }
|
||||
|
||||
// ── QA + Befunde beheben (je Ebene) ──────────────────────────────────────────────
|
||||
const qaBusy = ref(false)
|
||||
const guideRefresh = ref(0) // QA/Repair schreiben auch den Guide-Report → Badge neu laden
|
||||
async function runQaClick() {
|
||||
if (qaBusy.value) return
|
||||
qaBusy.value = true
|
||||
try { await runQa(props.topic) }
|
||||
finally { qaBusy.value = false; pollBoard(); guideRefresh.value++ }
|
||||
}
|
||||
|
||||
const repairBusy = ref('') // '' | 'inventory' | 'artefacts'
|
||||
const repairInfo = ref({}) // { inventory, artefacts }
|
||||
async function repairClick(ebene) {
|
||||
if (repairBusy.value) return
|
||||
repairBusy.value = ebene
|
||||
repairInfo.value = { ...repairInfo.value, [ebene]: '' }
|
||||
try {
|
||||
await runQa(props.topic)
|
||||
const r = await runRepair(props.topic, ebene)
|
||||
const n = (r.hygiene || []).length + (r.merges || []).length + (r.sub_merges || []).length
|
||||
+ (r.entfernt || []).length + (r.aufgeraeumt || 0) + (r.freigesprochen || []).length
|
||||
repairInfo.value = { ...repairInfo.value, [ebene]: n === 0 ? 'keine behebbaren Befunde'
|
||||
: `${(r.hygiene || []).length} Titel · ${(r.merges || []).length} Merges · ${(r.sub_merges || []).length} Sub-Merges · ${(r.entfernt || []).length} entfernt · ${r.aufgeraeumt || 0} aufgeräumt` }
|
||||
} catch (e) {
|
||||
repairInfo.value = { ...repairInfo.value, [ebene]: String(e.message || e) }
|
||||
} finally {
|
||||
qaBusy.value = false
|
||||
pollBoard()
|
||||
guideRefresh.value++
|
||||
repairBusy.value = ''; pollBoard()
|
||||
}
|
||||
}
|
||||
|
||||
const repairBusy = ref(false)
|
||||
const repairInfo = ref('')
|
||||
async function repairClick() {
|
||||
if (repairBusy.value) return
|
||||
repairBusy.value = true
|
||||
repairInfo.value = ''
|
||||
try {
|
||||
const r = await runRepair(props.topic)
|
||||
const n = (r.hygiene || []).length + (r.merges || []).length + (r.sub_merges || []).length
|
||||
+ (r.entfernt || []).length + (r.aufgeraeumt || 0) + (r.freigesprochen || []).length
|
||||
repairInfo.value = n === 0
|
||||
? 'keine behebbaren Befunde'
|
||||
: `${(r.hygiene || []).length} Titel · ${(r.merges || []).length} Merges · ${(r.sub_merges || []).length} Sub-Merges · ${(r.entfernt || []).length} entfernt · ${r.aufgeraeumt || 0} aufgeräumt · ${(r.freigesprochen || []).length} freigesprochen`
|
||||
} catch (e) {
|
||||
repairInfo.value = String(e.message || e)
|
||||
} finally {
|
||||
repairBusy.value = false
|
||||
pollBoard()
|
||||
}
|
||||
function removeArtefactsClick() {
|
||||
later(() => removeArtefacts(props.topic).then(pollBoard))
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -183,92 +146,69 @@ async function repairClick() {
|
||||
</header>
|
||||
|
||||
<div v-if="pollError" class="gen-poll-error">Board nicht erreichbar: {{ pollError }}</div>
|
||||
<div v-if="qa && qa.pausiert && qa.note != null" class="qa-pause">
|
||||
<strong>QA-Gate: Note {{ qa.note.toFixed(1) }} unter Schwelle {{ qa.schwelle }} — pausiert.</strong>
|
||||
<span v-if="qa.befunde?.length"> Befunde: {{ qa.befunde.join(' · ') }}</span>
|
||||
<button class="gen-act" @click="emit('continueAll', { qaForce: true })">Trotzdem fortsetzen</button>
|
||||
</div>
|
||||
|
||||
<section class="gen-section">
|
||||
<div class="gen-steps-top">
|
||||
<span class="gen-title">Bausteine</span>
|
||||
<div v-if="progress" class="gen-progress"><span class="gen-progress-dot"></span>{{ progress }}</div>
|
||||
<span v-if="runLaufzeit" class="gen-run" :title="runTokenTitel">⏱ {{ runLaufzeit }}<template v-if="runTokens"> · {{ runTokens }} Tokens</template></span>
|
||||
<span v-if="run?.fails?.length" class="gen-run-fail" :title="run.fails.map((f) => f.key + ': ' + f.error).join('\n')">⚠ {{ run.fails.length }} Fehler</span>
|
||||
<div v-if="!generating" class="gen-actions">
|
||||
<button class="gen-act" :disabled="qaBusy" title="QA-Lauf wie am Gate (inkl. LLM-Stichprobe)"
|
||||
@click="runQaClick">{{ qaBusy ? 'QA läuft…' : 'QA' }}</button>
|
||||
<button v-if="qa" class="gen-act" :disabled="repairBusy"
|
||||
title="QA-Befunde gezielt beheben: Hygiene, bestätigte Dubletten mergen, Fremd/Unecht nach Gegen-Judge entfernen"
|
||||
@click="repairClick">{{ repairBusy ? 'Repariert…' : 'Befunde beheben' }}</button>
|
||||
<span v-if="repairInfo" class="repair-info">{{ repairInfo }}</span>
|
||||
<button class="gen-act play" @click="emit('restartAll')">{{ ready || partial ? '+ Recherche' : 'Generieren' }}</button>
|
||||
<button v-if="partial" class="gen-act" @click="emit('continueAll')">Fortsetzen</button>
|
||||
<button
|
||||
v-if="ready || partial || error"
|
||||
class="gen-act danger"
|
||||
:class="{ armed: isArmed('remove') }"
|
||||
:title="error ? 'Räumt Crawl/Reste auf — danach neu generierbar' : ''"
|
||||
@click="armOrRun('remove', () => later(() => emit('removeAll')))"
|
||||
>{{ isArmed('remove') ? 'Sicher?' : 'Entfernen' }}</button>
|
||||
</div>
|
||||
<div v-else class="gen-actions">
|
||||
<div v-if="generating" class="gen-actions">
|
||||
<button class="gen-act" @click="emit('addResearch')">+ Recherche</button>
|
||||
<button class="gen-act danger" @click="emit('cancel')">Abbrechen</button>
|
||||
</div>
|
||||
<button
|
||||
v-if="dead.length"
|
||||
v-if="dead.length && generating"
|
||||
class="gen-act"
|
||||
:title="dead.map((d) => d.title + ': ' + d.error).join('\n')"
|
||||
@click="later(() => emit('requeueDead'))"
|
||||
>⟳ {{ dead.length }} dead</button>
|
||||
</div>
|
||||
|
||||
<!-- Ebene Inventar -->
|
||||
<div class="gen-board-label">
|
||||
Inventar
|
||||
<span v-if="qa && qa.note != null" class="qa-note" :class="qa.note >= qa.schwelle ? 'ok' : 'bad'"
|
||||
:title="'QA-Schwelle ' + qa.schwelle">QA {{ qa.note.toFixed(1) }}/10</span>
|
||||
<span v-if="qa && qa.note != null" class="qa-note" :class="qa.note >= 10 ? 'ok' : 'bad'">QA {{ pct(qa.note) }}</span>
|
||||
<label class="auto-box" title="Nach der QA automatisch „Befunde beheben“, bis 100 % (max 10×)">
|
||||
<input type="checkbox" :checked="autoInventory" @change="emit('setAuto', { ebene: 'inventory', value: $event.target.checked })"> Auto
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="!generating" class="gen-bar">
|
||||
<button class="gen-act" :disabled="qaBusy" @click="runQaClick">{{ qaBusy ? 'QA läuft…' : 'QA' }}</button>
|
||||
<button v-if="qa" class="gen-act" :disabled="!!repairBusy" @click="repairClick('inventory')">{{ repairBusy === 'inventory' ? 'Repariert…' : 'Befunde beheben' }}</button>
|
||||
<span v-if="repairInfo.inventory" class="repair-info">{{ repairInfo.inventory }}</span>
|
||||
<button class="gen-act play" @click="emit('restartAll')">{{ ready || partial ? '+ Recherche' : 'Generieren' }}</button>
|
||||
<button v-if="partial" class="gen-act" @click="emit('continueAll')">Fortsetzen</button>
|
||||
<button v-if="ready || partial || error" class="gen-act danger" :class="{ armed: isArmed('remove') }"
|
||||
title="Löscht das gesamte Topic-Inventar (Artefakte + Guide hängen daran)"
|
||||
@click="armOrRun('remove', () => later(() => emit('removeAll')))">{{ isArmed('remove') ? 'Sicher?' : 'Entfernen' }}</button>
|
||||
</div>
|
||||
<ProgressBar v-if="inventoryProgress.total" :value="inventoryProgress.value"
|
||||
:label="`${inventoryProgress.done}/${inventoryProgress.total} Karten fertig · ${Math.round(inventoryProgress.value * 100)} %`"
|
||||
:hint="scopeGrowing ? 'Umfang wächst noch' : ''" />
|
||||
<KanbanBoard
|
||||
:columns="inventoryCols"
|
||||
:agents="board?.agents || []"
|
||||
:generating="generating"
|
||||
:selectable="!generating"
|
||||
:selectedKey="sel?.board === 'inventory' ? sel.key : null"
|
||||
@stageClick="stageClick"
|
||||
/>
|
||||
<KanbanBoard :columns="inventoryCols" :agents="board?.agents || []" :generating="generating" />
|
||||
|
||||
<!-- Ebene Artefakte -->
|
||||
<div class="gen-board-label">
|
||||
Artefakte
|
||||
<span v-if="qa && qa.note_artefakte != null" class="qa-note"
|
||||
:class="qa.note_artefakte >= qa.schwelle ? 'ok' : 'bad'"
|
||||
title="Beleg-Quote + verwaiste Artefakte">QA {{ qa.note_artefakte.toFixed(1) }}/10</span>
|
||||
<span v-if="qa && qa.note_artefakte != null" class="qa-note" :class="qa.note_artefakte >= 10 ? 'ok' : 'bad'">QA {{ pct(qa.note_artefakte) }}</span>
|
||||
<label class="auto-box" title="Nach der QA automatisch „Befunde beheben“, bis 100 % (max 10×)">
|
||||
<input type="checkbox" :checked="autoArtefacts" @change="emit('setAuto', { ebene: 'artefacts', value: $event.target.checked })"> Auto
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="!generating" class="gen-bar">
|
||||
<button class="gen-act" :disabled="qaBusy" @click="runQaClick">{{ qaBusy ? 'QA läuft…' : 'QA' }}</button>
|
||||
<button v-if="qa" class="gen-act" :disabled="!!repairBusy" @click="repairClick('artefacts')">{{ repairBusy === 'artefacts' ? 'Repariert…' : 'Befunde beheben' }}</button>
|
||||
<span v-if="repairInfo.artefacts" class="repair-info">{{ repairInfo.artefacts }}</span>
|
||||
<button class="gen-act play" title="Board 2 auf dem fertigen Inventar bauen (Fortsetzen)" @click="emit('continueAll')">Generieren</button>
|
||||
<button v-if="ready || partial" class="gen-act danger" :class="{ armed: isArmed('remove-art') }"
|
||||
title="Nur die Artefakte leeren — Inventar bleibt"
|
||||
@click="armOrRun('remove-art', removeArtefactsClick)">{{ isArmed('remove-art') ? 'Sicher?' : 'Entfernen' }}</button>
|
||||
</div>
|
||||
<ProgressBar v-if="artefactProgress.total" :value="artefactProgress.value"
|
||||
:label="`${artefactProgress.done}/${artefactProgress.total} Karten fertig · ${Math.round(artefactProgress.value * 100)} %`"
|
||||
:hint="scopeGrowing ? 'Umfang wächst noch' : ''" />
|
||||
<KanbanBoard
|
||||
:columns="artefactCols"
|
||||
:generating="generating"
|
||||
:selectable="!generating"
|
||||
:cardSelectable="!generating"
|
||||
:selectedKey="sel?.board === 'artefacts' ? sel.key : null"
|
||||
@stageClick="stageClick"
|
||||
@cardClick="cardClick"
|
||||
/>
|
||||
|
||||
<div v-if="selCard && !generating" class="gen-step-actions">
|
||||
<span class="gen-step-actions-label">Karte «{{ selCard.title }}»:</span>
|
||||
<button class="gen-act play" :class="{ armed: isArmed('card') }" @click="armOrRun('card', restartCard)">{{ isArmed('card') ? 'Sicher?' : '↻ Karte neu generieren' }}</button>
|
||||
<button class="gen-act ghost" @click="selCard = null; resetConfirm()">Abbrechen</button>
|
||||
</div>
|
||||
<div v-if="sel && !generating" class="gen-step-actions">
|
||||
<span class="gen-step-actions-label">Ab «{{ sel.label }}»:</span>
|
||||
<button class="gen-act play" @click="resetHere(true)">↻ neu generieren</button>
|
||||
<button class="gen-act danger" :class="{ armed: isArmed('reset') }" @click="armOrRun('reset', () => resetHere(false))">{{ isArmed('reset') ? 'Sicher?' : '✕ nur zurücksetzen' }}</button>
|
||||
<button class="gen-act ghost" @click="sel = null; resetConfirm()">Abbrechen</button>
|
||||
</div>
|
||||
<KanbanBoard :columns="artefactCols" :agents="board?.agents || []" :generating="generating" />
|
||||
</section>
|
||||
|
||||
<section class="gen-section">
|
||||
@@ -276,12 +216,12 @@ async function repairClick() {
|
||||
:topic="topic"
|
||||
:format="guideFormat"
|
||||
:refresh="guideRefresh"
|
||||
:auto="autoGuide"
|
||||
@cancelGuide="(id) => emit('cancelGuide', id)"
|
||||
@startGuide="(p) => emit('startGuide', p)"
|
||||
@resetStage="(p) => emit('resetGuideStage', p)"
|
||||
@preview="emit('preview')"
|
||||
@removeFormat="(f) => emit('removeFormat', f)"
|
||||
@resetCard="(p) => emit('resetGuideCard', p)"
|
||||
@setAuto="(v) => emit('setAuto', { ebene: 'guide', value: v })"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
@@ -330,12 +270,15 @@ async function repairClick() {
|
||||
}
|
||||
.gen-title { font-size: 0.9rem; font-weight: 700; }
|
||||
.gen-board-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
font-size: 0.64rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-faint);
|
||||
margin: 0.5rem 0 0.3rem;
|
||||
margin: 0.7rem 0 0.3rem;
|
||||
}
|
||||
.gen-progress {
|
||||
display: flex;
|
||||
@@ -355,16 +298,20 @@ async function repairClick() {
|
||||
|
||||
.gen-steps-top { display: flex; align-items: center; gap: 1rem; min-height: 1.9rem; margin-bottom: 0.4rem; }
|
||||
.gen-actions { margin-left: auto; display: flex; gap: 0.4rem; }
|
||||
.gen-bar { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; margin: 0.1rem 0 0.4rem; }
|
||||
|
||||
.gen-step-actions {
|
||||
display: flex;
|
||||
.auto-box {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.7rem;
|
||||
border-top: 1px dashed var(--border-strong);
|
||||
gap: 0.25rem;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.gen-step-actions-label { font-size: 0.8rem; font-weight: 600; color: var(--text-muted); }
|
||||
.auto-box input { cursor: pointer; margin: 0; }
|
||||
|
||||
.gen-act {
|
||||
border: 1px solid var(--border-strong);
|
||||
@@ -377,16 +324,18 @@ async function repairClick() {
|
||||
font-weight: 600;
|
||||
}
|
||||
.gen-act:hover { border-color: var(--accent); }
|
||||
.gen-act:disabled { opacity: 0.55; cursor: default; }
|
||||
.gen-act.play { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
|
||||
.gen-act.play:hover { background: var(--accent-hover); }
|
||||
.gen-act.danger { color: var(--danger); border-color: var(--danger); background: transparent; }
|
||||
.gen-act.danger.armed { background: var(--danger); color: #fff; }
|
||||
.gen-act.ghost { color: var(--text-muted); }
|
||||
.qa-note {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
padding: 0.1rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.qa-note.ok { background: color-mix(in srgb, #22c55e 18%, transparent); color: #16a34a; }
|
||||
.qa-note.bad { background: color-mix(in srgb, #ef4444 18%, transparent); color: #dc2626; }
|
||||
@@ -394,16 +343,4 @@ async function repairClick() {
|
||||
.gen-poll-error { color: var(--danger); font-size: 0.82rem; padding: 0.3rem 0; }
|
||||
.gen-run { font-size: 0.8rem; color: var(--text-muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
|
||||
.gen-run-fail { font-size: 0.78rem; color: var(--danger); white-space: nowrap; }
|
||||
.qa-pause {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.5rem 0.8rem;
|
||||
margin-bottom: 0.8rem;
|
||||
border: 1px solid color-mix(in srgb, #ef4444 40%, transparent);
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, #ef4444 8%, transparent);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, onUnmounted } from 'vue'
|
||||
import { fetchGuideBoard, fetchRuns, repairGuideBoard } from '../api.js'
|
||||
import { fetchGuideBoard, fetchRuns, repairGuideBoard, runQa } from '../api.js'
|
||||
import { usePolling } from '../composables/usePolling.js'
|
||||
import { useConfirm } from '../composables/useConfirm.js'
|
||||
import { fmtRuntime, fmtTokens } from '../format.js'
|
||||
@@ -11,8 +11,9 @@ const props = defineProps({
|
||||
topic: { type: String, required: true },
|
||||
format: { type: String, default: 'Guide' },
|
||||
refresh: { type: Number, default: 0 }, // Eltern-Signal: QA/Repair schrieben einen Guide-Report
|
||||
auto: { type: Boolean, default: true },
|
||||
})
|
||||
const emit = defineEmits(['cancelGuide', 'startGuide', 'resetStage', 'preview', 'removeFormat', 'resetCard'])
|
||||
const emit = defineEmits(['cancelGuide', 'startGuide', 'preview', 'removeFormat', 'setAuto'])
|
||||
|
||||
const board = ref(null)
|
||||
const pollError = ref(null)
|
||||
@@ -22,13 +23,10 @@ async function poll() {
|
||||
board.value = await fetchGuideBoard(props.topic, props.format)
|
||||
pollError.value = null
|
||||
} catch (e) {
|
||||
// 404 = Board noch nicht gebaut; echte Fehler (500/Netz) sichtbar machen
|
||||
if (e.status === 404) board.value = null
|
||||
else pollError.value = e.message
|
||||
}
|
||||
}
|
||||
// startPoll bleibt für die manuellen Starts nach startGuide/repairClick exponiert;
|
||||
// usePolling pausiert im Hintergrund-Tab und stoppt selbst, sobald generating false wird.
|
||||
const { start: startPoll } = usePolling(poll, () => !!board.value?.generating, 1200)
|
||||
|
||||
watch(() => props.topic, () => { board.value = null; poll() }, { immediate: true })
|
||||
@@ -69,34 +67,16 @@ watch(() => generating.value, (g) => {
|
||||
watch(() => props.topic, () => { run.value = null; loadRun() })
|
||||
onUnmounted(() => { if (clock) clearInterval(clock) })
|
||||
|
||||
// Stage-Index für ab_step (Reihenfolge = Spalten ohne "done").
|
||||
const STAGES = ['lernziele', 'zuweisung', 'writer', 'pruefer', 'fix']
|
||||
const sel = ref(null)
|
||||
const selCard = ref(null)
|
||||
const { isArmed, armOrRun, reset: resetConfirm } = useConfirm() // 2-Klick-Bestätigung (mit 3s-Auto-Reset)
|
||||
const { isArmed, armOrRun } = useConfirm() // 2-Klick-Bestätigung
|
||||
|
||||
function stageClick(c) {
|
||||
if (generating.value || !STAGES.includes(c.key)) return
|
||||
resetConfirm()
|
||||
selCard.value = null
|
||||
sel.value = sel.value?.key === c.key ? null : { key: c.key, label: c.label, idx: STAGES.indexOf(c.key) }
|
||||
const qaBusy = ref(false)
|
||||
async function runQaClick() {
|
||||
if (qaBusy.value) return
|
||||
qaBusy.value = true
|
||||
try { await runQa(props.topic) }
|
||||
finally { qaBusy.value = false; poll() }
|
||||
}
|
||||
|
||||
function cardClick(k) {
|
||||
if (generating.value || !k.card_id) return
|
||||
resetConfirm()
|
||||
sel.value = null
|
||||
const idx = Math.max(0, STAGES.indexOf(k.column))
|
||||
selCard.value = selCard.value?.card_id === k.card_id ? null : { ...k, idx }
|
||||
}
|
||||
|
||||
function resetCardHere() {
|
||||
const k = selCard.value
|
||||
selCard.value = null
|
||||
resetConfirm()
|
||||
emit('resetCard', { format: props.format, blockNorm: k.card_id, abStage: 0 })
|
||||
setTimeout(poll, 400)
|
||||
}
|
||||
const repairBusy = ref(false)
|
||||
const repairInfo = ref('')
|
||||
async function repairClick() {
|
||||
@@ -114,26 +94,17 @@ async function repairClick() {
|
||||
repairBusy.value = false
|
||||
}
|
||||
}
|
||||
function restartHere() {
|
||||
const s = sel.value
|
||||
sel.value = null
|
||||
emit('startGuide', { format: props.format, abStep: s.idx })
|
||||
startPoll()
|
||||
}
|
||||
function resetHere() {
|
||||
const s = sel.value
|
||||
sel.value = null
|
||||
emit('resetStage', { format: props.format, abStage: s.idx })
|
||||
setTimeout(poll, 400)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="gb-board">
|
||||
<div class="gb-top">
|
||||
<span class="gb-title">Guide · {{ format }}</span>
|
||||
<span v-if="board?.qa_guide != null" class="gb-qa" :class="board.qa_guide >= 9 ? 'ok' : 'bad'"
|
||||
title="Guide-QA (make qa-guide)">QA {{ board.qa_guide.toFixed(1) }}/10</span>
|
||||
<span v-if="board?.qa_guide != null" class="gb-qa" :class="board.qa_guide >= 10 ? 'ok' : 'bad'"
|
||||
title="Guide-QA">QA {{ Math.round(board.qa_guide * 10) }} %</span>
|
||||
<label class="auto-box" title="Nach der QA automatisch „Befunde beheben“, bis 100 % (max 10×)">
|
||||
<input type="checkbox" :checked="auto" @change="emit('setAuto', $event.target.checked)"> Auto
|
||||
</label>
|
||||
<span v-if="total" class="gb-count">{{ done }}/{{ total }} Karten fertig</span>
|
||||
<span v-if="runLaufzeit" class="gb-count">⏱ {{ runLaufzeit }}<template v-if="runTokens"> · {{ runTokens }} Tokens</template></span>
|
||||
<div v-if="board?.progress && generating" class="gb-progress"><span class="gb-progress-dot"></span>{{ board.progress }}</div>
|
||||
@@ -144,11 +115,12 @@ function resetHere() {
|
||||
<button class="gb-act danger" @click="emit('cancelGuide', board?.guide_id)">Abbrechen</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button class="gb-act play" @click="emit('startGuide', { format, abStep: null }); startPoll()">{{ total && done < total ? 'Fortsetzen' : total ? 'Neu generieren' : 'Generieren' }}</button>
|
||||
<button v-if="done === total && total" class="gb-act" @click="emit('preview')">Guide öffnen</button>
|
||||
<button class="gb-act" :disabled="qaBusy" @click="runQaClick">{{ qaBusy ? 'QA läuft…' : 'QA' }}</button>
|
||||
<button v-if="total && board?.qa_guide != null && board.qa_guide < 10" class="gb-act"
|
||||
:disabled="repairBusy" @click="repairClick">{{ repairBusy ? 'Repariert…' : 'Befunde beheben' }}</button>
|
||||
<span v-if="repairInfo" class="gb-count">{{ repairInfo }}</span>
|
||||
<button class="gb-act play" @click="emit('startGuide', { format }); startPoll()">{{ total && done < total ? 'Fortsetzen' : total ? 'Neu generieren' : 'Generieren' }}</button>
|
||||
<button v-if="done === total && total" class="gb-act" @click="emit('preview')">Guide öffnen</button>
|
||||
<button v-if="total" class="gb-act danger" :class="{ armed: isArmed('delete') }" @click="armOrRun('delete', () => { emit('removeFormat', format); setTimeout(poll, 600) })">{{ isArmed('delete') ? 'Sicher?' : 'Entfernen' }}</button>
|
||||
</template>
|
||||
</div>
|
||||
@@ -157,28 +129,7 @@ function resetHere() {
|
||||
<ProgressBar v-if="total" :value="progressValue"
|
||||
:label="`${done}/${total} Karten fertig · ${Math.round(progressValue * 100)} %`" />
|
||||
|
||||
<KanbanBoard
|
||||
:columns="columns"
|
||||
:agents="board?.agents || []"
|
||||
:generating="generating"
|
||||
:selectable="!generating"
|
||||
:cardSelectable="!generating"
|
||||
:selectedKey="sel?.key || null"
|
||||
@stageClick="stageClick"
|
||||
@cardClick="cardClick"
|
||||
/>
|
||||
|
||||
<div v-if="selCard && !generating" class="gb-stage-actions">
|
||||
<span class="gb-stage-label">Karte «{{ selCard.title }}»:</span>
|
||||
<button class="gb-act play" :class="{ armed: isArmed('card') }" @click="armOrRun('card', resetCardHere)">{{ isArmed('card') ? 'Sicher?' : '↻ Karte neu (ab Lernziele)' }}</button>
|
||||
<button class="gb-act ghost" @click="selCard = null; resetConfirm()">Abbrechen</button>
|
||||
</div>
|
||||
<div v-if="sel && !generating" class="gb-stage-actions">
|
||||
<span class="gb-stage-label">Ab «{{ sel.label }}»:</span>
|
||||
<button class="gb-act play" @click="restartHere">↻ neu generieren</button>
|
||||
<button class="gb-act danger" :class="{ armed: isArmed('reset') }" @click="armOrRun('reset', resetHere)">{{ isArmed('reset') ? 'Sicher?' : '✕ nur zurücksetzen' }}</button>
|
||||
<button class="gb-act ghost" @click="sel = null; resetConfirm()">Abbrechen</button>
|
||||
</div>
|
||||
<KanbanBoard :columns="columns" :agents="board?.agents || []" :generating="generating" />
|
||||
|
||||
<div v-if="!total && !generating" class="gb-empty">Noch kein Board — «Generieren» erzeugt eine Karte je Baustein und schiebt sie live durch die Spalten.</div>
|
||||
</section>
|
||||
@@ -186,7 +137,7 @@ function resetHere() {
|
||||
|
||||
<style scoped>
|
||||
.gb-board { padding: 0.85rem 0 0; }
|
||||
.gb-top { display: flex; align-items: center; gap: 1rem; min-height: 1.9rem; margin-bottom: 0.6rem; }
|
||||
.gb-top { display: flex; align-items: center; gap: 1rem; min-height: 1.9rem; margin-bottom: 0.6rem; flex-wrap: wrap; }
|
||||
.gb-title { font-size: 0.9rem; font-weight: 700; }
|
||||
.gb-count { color: var(--text-muted); font-size: 0.82rem; }
|
||||
.gb-qa {
|
||||
@@ -197,6 +148,16 @@ function resetHere() {
|
||||
}
|
||||
.gb-qa.ok { background: color-mix(in srgb, #22c55e 18%, transparent); color: #16a34a; }
|
||||
.gb-qa.bad { background: color-mix(in srgb, #ef4444 18%, transparent); color: #dc2626; }
|
||||
.auto-box {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
.auto-box input { cursor: pointer; margin: 0; }
|
||||
.gb-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -213,17 +174,8 @@ function resetHere() {
|
||||
animation: pulse-soft 1.2s ease-in-out infinite;
|
||||
}
|
||||
.gb-error { color: var(--danger); font-size: 0.82rem; }
|
||||
.gb-actions { margin-left: auto; display: flex; gap: 0.4rem; }
|
||||
.gb-actions { margin-left: auto; display: flex; gap: 0.4rem; flex-wrap: wrap; }
|
||||
|
||||
.gb-stage-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.7rem;
|
||||
border-top: 1px dashed var(--border-strong);
|
||||
}
|
||||
.gb-stage-label { font-size: 0.8rem; font-weight: 600; color: var(--text-muted); }
|
||||
.gb-empty { color: var(--text-faint); font-size: 0.85rem; padding: 1rem 0; }
|
||||
|
||||
.gb-act {
|
||||
@@ -237,9 +189,9 @@ function resetHere() {
|
||||
font-weight: 600;
|
||||
}
|
||||
.gb-act:hover { border-color: var(--accent); }
|
||||
.gb-act:disabled { opacity: 0.55; cursor: default; }
|
||||
.gb-act.play { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
|
||||
.gb-act.play:hover { background: var(--accent-hover); }
|
||||
.gb-act.danger { color: var(--danger); border-color: var(--danger); background: transparent; }
|
||||
.gb-act.danger.armed { background: var(--danger); color: #fff; }
|
||||
.gb-act.ghost { color: var(--text-muted); }
|
||||
</style>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<script setup>
|
||||
// Gemeinsame Live-Board-Komponente (Blocks + Guide): Spalten mit Count-Badge und
|
||||
// Karten-Titeln. Spaltenkopf-Klick (wenn erlaubt) → stageClick für Reset-Aktionen.
|
||||
// Karten-Titeln. Reines Display — Steuerung läuft über die Ebenen-Buttons.
|
||||
import { fmtRuntime } from '../format.js'
|
||||
|
||||
const props = defineProps({
|
||||
columns: { type: Array, default: () => [] }, // [{key, board?, label, total, cards:[{title,status,info,retries?,rounds?,ziele?}]}]
|
||||
agents: { type: Array, default: () => [] }, // [{label, runtime}]
|
||||
generating: { type: Boolean, default: false },
|
||||
selectable: { type: Boolean, default: false }, // Spaltenkopf klickbar (Reset ab Spalte)
|
||||
cardSelectable: { type: Boolean, default: false }, // Karten klickbar (Einzel-Restart)
|
||||
selectedKey: { type: String, default: null },
|
||||
})
|
||||
const emit = defineEmits(['stageClick', 'cardClick'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -25,23 +21,17 @@ const emit = defineEmits(['stageClick', 'cardClick'])
|
||||
v-for="c in columns"
|
||||
:key="(c.board || '') + c.key"
|
||||
class="kb-col"
|
||||
:class="{ active: c.total > 0, sel: selectedKey === c.key, collapsed: !c.total }"
|
||||
:class="{ active: c.total > 0, collapsed: !c.total }"
|
||||
>
|
||||
<button
|
||||
class="kb-col-head"
|
||||
:disabled="!selectable"
|
||||
:title="selectable ? `Aktionen ab «${c.label}»` : c.label"
|
||||
@click="selectable && emit('stageClick', c)"
|
||||
>
|
||||
<div class="kb-col-head" :title="c.label">
|
||||
<span class="kb-col-label">{{ c.label }}</span>
|
||||
<span class="kb-col-count" :class="{ zero: !c.total }">{{ c.total }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<ul v-if="c.cards && c.cards.length" class="kb-cards">
|
||||
<li
|
||||
v-for="(k, i) in c.cards" :key="i" class="kb-card"
|
||||
:class="[k.status, { klickbar: cardSelectable && k.card_id }]"
|
||||
:title="cardSelectable && k.card_id ? `${k.title} — Klick: Karte neu generieren` : (k.info || k.title)"
|
||||
@click="cardSelectable && k.card_id && emit('cardClick', { ...k, column: c.key, colBoard: c.board })"
|
||||
:class="k.status"
|
||||
:title="k.info || k.title"
|
||||
>
|
||||
<div class="kb-card-row">
|
||||
<span class="kb-dot" :class="[k.status, { pulse: generating && k.status === 'active' }]"></span>
|
||||
|
||||
Reference in New Issue
Block a user