update
This commit is contained in:
@@ -805,6 +805,9 @@ async def remove_guide_format(req: GuideFormatRequest):
|
|||||||
for p in guide_slot_files(content):
|
for p in guide_slot_files(content):
|
||||||
p.unlink(missing_ok=True)
|
p.unlink(missing_ok=True)
|
||||||
content.unlink(missing_ok=True)
|
content.unlink(missing_ok=True)
|
||||||
|
import qa # Guide-QA-Report mitlöschen — sonst zeigt board_snapshot die alte Note weiter
|
||||||
|
for p in qa.report_paths(req.topic, guide=True):
|
||||||
|
p.unlink(missing_ok=True)
|
||||||
return {"ok": True, "removed": len(doomed)}
|
return {"ok": True, "removed": len(doomed)}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -114,16 +114,30 @@ async function runQaClick() {
|
|||||||
|
|
||||||
const repairBusy = ref('') // '' | 'inventory' | 'artefacts'
|
const repairBusy = ref('') // '' | 'inventory' | 'artefacts'
|
||||||
const repairInfo = ref({}) // { inventory, artefacts }
|
const repairInfo = ref({}) // { inventory, artefacts }
|
||||||
|
function repairText(r) {
|
||||||
|
const n = (r.hygiene || []).length + (r.merges || []).length + (r.sub_merges || []).length
|
||||||
|
+ (r.entfernt || []).length + (r.aufgeraeumt || 0) + (r.freigesprochen || []).length
|
||||||
|
return 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`
|
||||||
|
}
|
||||||
|
// Auto an → „Befunde beheben"-Loop bis 100 % / Stillstand / 10×; sonst genau ein Durchlauf.
|
||||||
async function repairClick(ebene) {
|
async function repairClick(ebene) {
|
||||||
if (repairBusy.value) return
|
if (repairBusy.value) return
|
||||||
|
const auto = ebene === 'inventory' ? props.autoInventory : props.autoArtefacts
|
||||||
|
const max = auto ? 10 : 1
|
||||||
repairBusy.value = ebene
|
repairBusy.value = ebene
|
||||||
repairInfo.value = { ...repairInfo.value, [ebene]: '' }
|
repairInfo.value = { ...repairInfo.value, [ebene]: '' }
|
||||||
|
let prev = null
|
||||||
try {
|
try {
|
||||||
const r = await runRepair(props.topic, ebene)
|
for (let i = 0; i < max; i++) {
|
||||||
const n = (r.hygiene || []).length + (r.merges || []).length + (r.sub_merges || []).length
|
const r = await runRepair(props.topic, ebene)
|
||||||
+ (r.entfernt || []).length + (r.aufgeraeumt || 0) + (r.freigesprochen || []).length
|
const note = ebene === 'artefacts' ? r.note_artefakte : r.note
|
||||||
repairInfo.value = { ...repairInfo.value, [ebene]: n === 0 ? 'keine behebbaren Befunde'
|
const suffix = max > 1 ? ` — Runde ${i + 1}${note != null ? ` (${Math.round(note * 10)} %)` : ''}` : ''
|
||||||
: `${(r.hygiene || []).length} Titel · ${(r.merges || []).length} Merges · ${(r.sub_merges || []).length} Sub-Merges · ${(r.entfernt || []).length} entfernt · ${r.aufgeraeumt || 0} aufgeräumt` }
|
repairInfo.value = { ...repairInfo.value, [ebene]: repairText(r) + suffix }
|
||||||
|
if (note == null || note >= 10) break // 100 % erreicht
|
||||||
|
if (prev != null && note <= prev) break // Stillstand: Repair bewegt nichts mehr
|
||||||
|
prev = note
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
repairInfo.value = { ...repairInfo.value, [ebene]: String(e.message || e) }
|
repairInfo.value = { ...repairInfo.value, [ebene]: String(e.message || e) }
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ const columns = computed(() => board.value?.columns || [])
|
|||||||
const total = computed(() => columns.value.reduce((n, c) => n + c.total, 0))
|
const total = computed(() => columns.value.reduce((n, c) => n + c.total, 0))
|
||||||
const done = computed(() => columns.value.find((c) => c.key === 'done')?.total || 0)
|
const done = computed(() => columns.value.find((c) => c.key === 'done')?.total || 0)
|
||||||
const progressValue = computed(() => (total.value ? done.value / total.value : 0))
|
const progressValue = computed(() => (total.value ? done.value / total.value : 0))
|
||||||
|
// QA-Badge nur wenn es ein Board gibt — sonst zeigt ein verwaister Report eine alte Note.
|
||||||
|
const qaNote = computed(() => (total.value ? board.value?.qa_guide : null))
|
||||||
|
|
||||||
// Laufzeit + Tokens aus /api/runs (deckt auch Guide-Läufe ab — beide setzen run_id)
|
// Laufzeit + Tokens aus /api/runs (deckt auch Guide-Läufe ab — beide setzen run_id)
|
||||||
const run = ref(null)
|
const run = ref(null)
|
||||||
@@ -77,53 +79,84 @@ async function runQaClick() {
|
|||||||
finally { qaBusy.value = false; poll() }
|
finally { qaBusy.value = false; poll() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeClick() {
|
||||||
|
emit('removeFormat', props.format)
|
||||||
|
board.value = null // optimistisch: Karten + QA-Badge sofort weg, poll bestätigt
|
||||||
|
setTimeout(poll, 800)
|
||||||
|
}
|
||||||
|
|
||||||
const repairBusy = ref(false)
|
const repairBusy = ref(false)
|
||||||
const repairInfo = ref('')
|
const repairInfo = ref('')
|
||||||
|
// Wartet, bis die Guide-Regenerierung der offenen Karten fertig ist (für den Auto-Loop).
|
||||||
|
function warteBisFertig(timeoutMs = 300000) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const t0 = Date.now()
|
||||||
|
const iv = setInterval(async () => {
|
||||||
|
await poll()
|
||||||
|
if (!board.value?.generating || Date.now() - t0 > timeoutMs) { clearInterval(iv); resolve() }
|
||||||
|
}, 2000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Auto an → Loop: Befunde beheben → regenerieren → QA, bis 100 % / Stillstand / 10×.
|
||||||
|
// Auto aus → genau ein Durchlauf (nur anstoßen).
|
||||||
async function repairClick() {
|
async function repairClick() {
|
||||||
|
if (repairBusy.value) return
|
||||||
|
const max = props.auto ? 10 : 1
|
||||||
repairBusy.value = true
|
repairBusy.value = true
|
||||||
repairInfo.value = ''
|
repairInfo.value = ''
|
||||||
|
let prev = null
|
||||||
try {
|
try {
|
||||||
const r = await repairGuideBoard(props.topic, props.format)
|
for (let i = 0; i < max; i++) {
|
||||||
repairInfo.value = (r.betroffen || []).length
|
const r = await repairGuideBoard(props.topic, props.format)
|
||||||
? `${r.betroffen.length} Abschnitt(e) → Prüfen`
|
if (!(r.betroffen || []).length) {
|
||||||
: 'keine Befunde im letzten QA-Report'
|
repairInfo.value = i === 0 ? 'keine Befunde im letzten QA-Report' : `fertig nach ${i} Runde(n)`
|
||||||
if ((r.betroffen || []).length) startPoll()
|
break
|
||||||
|
}
|
||||||
|
repairInfo.value = `Runde ${i + 1}: ${r.betroffen.length} Abschnitt(e) → Prüfen…`
|
||||||
|
startPoll()
|
||||||
|
if (max === 1) break // ohne Auto: nur anstoßen, nicht auf Fertigstellung warten
|
||||||
|
await warteBisFertig() // Regenerierung abwarten
|
||||||
|
await runQa(props.topic) // neue Guide-QA messen
|
||||||
|
await poll()
|
||||||
|
const note = board.value?.qa_guide
|
||||||
|
if (note == null || note >= 10) { repairInfo.value = `100 % nach ${i + 1} Runde(n)`; break }
|
||||||
|
if (prev != null && note <= prev) { repairInfo.value = `Stillstand bei ${Math.round(note * 10)} %`; break }
|
||||||
|
prev = note
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
repairInfo.value = String(e.message || e)
|
repairInfo.value = String(e.message || e)
|
||||||
} finally {
|
} finally {
|
||||||
repairBusy.value = false
|
repairBusy.value = false
|
||||||
|
poll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="gb-board">
|
<section class="gb-board">
|
||||||
<div class="gb-top">
|
<div class="gen-board-label">
|
||||||
<span class="gb-title">Guide · {{ format }}</span>
|
Guide · {{ format }}
|
||||||
<span v-if="board?.qa_guide != null" class="gb-qa" :class="board.qa_guide >= 10 ? 'ok' : 'bad'"
|
<span v-if="qaNote != null" class="qa-note" :class="qaNote >= 10 ? 'ok' : 'bad'">QA {{ Math.round(qaNote * 10) }} %</span>
|
||||||
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×)">
|
<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
|
<input type="checkbox" :checked="auto" @change="emit('setAuto', $event.target.checked)"> Auto
|
||||||
</label>
|
</label>
|
||||||
<span v-if="total" class="gb-count">{{ done }}/{{ total }} Karten fertig</span>
|
<span v-if="total" class="gb-count">{{ done }}/{{ total }} Karten</span>
|
||||||
<span v-if="runLaufzeit" class="gb-count">⏱ {{ runLaufzeit }}<template v-if="runTokens"> · {{ runTokens }} Tokens</template></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>
|
<div v-if="board?.progress && generating" class="gen-progress"><span class="gen-progress-dot"></span>{{ board.progress }}</div>
|
||||||
<div v-if="board?.error" class="gb-error">{{ board.error }}</div>
|
<div v-if="board?.error" class="gb-error">{{ board.error }}</div>
|
||||||
<div v-if="pollError" class="gb-error">Board nicht erreichbar: {{ pollError }}</div>
|
<div v-if="pollError" class="gb-error">Board nicht erreichbar: {{ pollError }}</div>
|
||||||
<div class="gb-actions">
|
</div>
|
||||||
<template v-if="generating">
|
|
||||||
<button class="gb-act danger" @click="emit('cancelGuide', board?.guide_id)">Abbrechen</button>
|
<div v-if="generating" class="gen-bar">
|
||||||
</template>
|
<button class="gen-act danger" @click="emit('cancelGuide', board?.guide_id)">Abbrechen</button>
|
||||||
<template v-else>
|
</div>
|
||||||
<button class="gb-act" :disabled="qaBusy" @click="runQaClick">{{ qaBusy ? 'QA läuft…' : 'QA' }}</button>
|
<div v-else class="gen-bar">
|
||||||
<button v-if="total && board?.qa_guide != null && board.qa_guide < 10" class="gb-act"
|
<button class="gen-act" :disabled="qaBusy" @click="runQaClick">{{ qaBusy ? 'QA läuft…' : 'QA' }}</button>
|
||||||
:disabled="repairBusy" @click="repairClick">{{ repairBusy ? 'Repariert…' : 'Befunde beheben' }}</button>
|
<button v-if="total && qaNote != null && qaNote < 10" class="gen-act" :disabled="repairBusy" @click="repairClick">{{ repairBusy ? 'Repariert…' : 'Befunde beheben' }}</button>
|
||||||
<span v-if="repairInfo" class="gb-count">{{ repairInfo }}</span>
|
<span v-if="repairInfo" class="repair-info">{{ repairInfo }}</span>
|
||||||
<button class="gb-act play" @click="emit('startGuide', { format }); startPoll()">{{ total && done < total ? 'Fortsetzen' : total ? 'Neu generieren' : 'Generieren' }}</button>
|
<button class="gen-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="done === total && total" class="gen-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>
|
<button v-if="total" class="gen-act danger" :class="{ armed: isArmed('delete') }" @click="armOrRun('delete', removeClick)">{{ isArmed('delete') ? 'Sicher?' : 'Entfernen' }}</button>
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ProgressBar v-if="total" :value="progressValue"
|
<ProgressBar v-if="total" :value="progressValue"
|
||||||
@@ -137,48 +170,63 @@ async function repairClick() {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.gb-board { padding: 0.85rem 0 0; }
|
.gb-board { padding: 0.85rem 0 0; }
|
||||||
.gb-top { display: flex; align-items: center; gap: 1rem; min-height: 1.9rem; margin-bottom: 0.6rem; flex-wrap: wrap; }
|
.gen-board-label {
|
||||||
.gb-title { font-size: 0.9rem; font-weight: 700; }
|
display: flex;
|
||||||
.gb-count { color: var(--text-muted); font-size: 0.82rem; }
|
align-items: center;
|
||||||
.gb-qa {
|
gap: 0.6rem;
|
||||||
|
font-size: 0.64rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--text-faint);
|
||||||
|
margin: 0.2rem 0 0.3rem;
|
||||||
|
}
|
||||||
|
.gen-bar { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; margin: 0.1rem 0 0.4rem; }
|
||||||
|
.gb-count { color: var(--text-muted); font-size: 0.82rem; text-transform: none; letter-spacing: 0; font-weight: 400; }
|
||||||
|
.qa-note {
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 0.1rem 0.5rem;
|
padding: 0.1rem 0.5rem;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
.gb-qa.ok { background: color-mix(in srgb, #22c55e 18%, transparent); color: #16a34a; }
|
.qa-note.ok { background: color-mix(in srgb, #22c55e 18%, transparent); color: #16a34a; }
|
||||||
.gb-qa.bad { background: color-mix(in srgb, #ef4444 18%, transparent); color: #dc2626; }
|
.qa-note.bad { background: color-mix(in srgb, #ef4444 18%, transparent); color: #dc2626; }
|
||||||
.auto-box {
|
.auto-box {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
font-size: 0.72rem;
|
font-size: 0.68rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.auto-box input { cursor: pointer; margin: 0; }
|
.auto-box input { cursor: pointer; margin: 0; }
|
||||||
.gb-progress {
|
.gen-progress {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
font-size: 0.84rem;
|
font-size: 0.84rem;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
.gb-progress-dot {
|
.gen-progress-dot {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
animation: pulse-soft 1.2s ease-in-out infinite;
|
animation: pulse-soft 1.2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
.gb-error { color: var(--danger); font-size: 0.82rem; }
|
.gb-error { color: var(--danger); font-size: 0.82rem; text-transform: none; letter-spacing: 0; font-weight: 400; }
|
||||||
.gb-actions { margin-left: auto; display: flex; gap: 0.4rem; flex-wrap: wrap; }
|
|
||||||
|
|
||||||
.gb-empty { color: var(--text-faint); font-size: 0.85rem; padding: 1rem 0; }
|
.gb-empty { color: var(--text-faint); font-size: 0.85rem; padding: 1rem 0; }
|
||||||
|
.repair-info { font-size: 0.78rem; color: var(--text-muted); }
|
||||||
|
|
||||||
.gb-act {
|
.gen-act {
|
||||||
border: 1px solid var(--border-strong);
|
border: 1px solid var(--border-strong);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
@@ -188,10 +236,10 @@ async function repairClick() {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.gb-act:hover { border-color: var(--accent); }
|
.gen-act:hover { border-color: var(--accent); }
|
||||||
.gb-act:disabled { opacity: 0.55; cursor: default; }
|
.gen-act:disabled { opacity: 0.55; cursor: default; }
|
||||||
.gb-act.play { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
|
.gen-act.play { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
|
||||||
.gb-act.play:hover { background: var(--accent-hover); }
|
.gen-act.play:hover { background: var(--accent-hover); }
|
||||||
.gb-act.danger { color: var(--danger); border-color: var(--danger); background: transparent; }
|
.gen-act.danger { color: var(--danger); border-color: var(--danger); background: transparent; }
|
||||||
.gb-act.danger.armed { background: var(--danger); color: #fff; }
|
.gen-act.danger.armed { background: var(--danger); color: #fff; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user