This commit is contained in:
Team3
2026-06-06 22:16:32 +02:00
parent 08e67cb4f1
commit 80b964fef4
5 changed files with 99 additions and 26 deletions

View File

@@ -635,13 +635,17 @@ async def _generate_onepager(
recherche_path = content_path.parent / f"{content_path.stem}.recherche.md"
fragment_paths.append(recherche_path)
recherche_path.unlink(missing_ok=True)
# Projekte bekommen eigene Recherche-Dimensionen — Produkt-Fragen
# (Version, Lizenz, Alternativen) laufen dort ins Leere.
if project:
source = _prompt("OnePager-Quelle-Projekt", project=project)
recherche_template = "OnePager-Recherche-Projekt"
else:
source = _prompt("OnePager-Quelle-Thema", topic=topic)
recherche_template = "OnePager-Recherche"
slots = [{
"key": f"{guide_id}-recherche",
"prompt": _prompt("OnePager-Recherche", topic=topic, source=source, out_path=recherche_path, extra=_extra(instructions)),
"prompt": _prompt(recherche_template, topic=topic, source=source, out_path=recherche_path, extra=_extra(instructions)),
"role": "quick", "capabilities": "files" if project else "full",
"payload": (lambda result: recherche_path.read_text(encoding="utf-8") if recherche_path.exists() else None),
}]

View File

@@ -122,6 +122,10 @@ async def remove_bausteine(topic: str):
async def create(req: GuideCreateRequest):
if req.format != "OnePager" and not bausteine_path(req.topic.strip()).exists():
raise HTTPException(400, "Erst Bausteine erstellen")
# Kein Duplikat-Start: pro Thema+Format höchstens eine laufende Generierung
for g in await list_guides():
if g["topic"] == req.topic.strip() and g["format"] == req.format and g["status"] in ("queued", "generating"):
raise HTTPException(409, "Generierung läuft bereits")
await create_topic(req.topic.strip())
now = datetime.now(timezone.utc).isoformat()
guide = {

View File

@@ -189,6 +189,12 @@ async function handleBausteineClick({ instructions }) {
async function handleFormatClick({ format, instructions }) {
if (!selectedTopic.value) return
// Kein Duplikat-Start: läuft für Thema+Format schon eine Generierung, ignorieren
const running = guides.value.some(
(g) => g.topic === selectedTopic.value && g.format === format
&& (g.status === 'generating' || g.status === 'queued'),
)
if (running) return
await apiCreate(selectedTopic.value, format, instructions, provider.value)
await loadGuides()
startPolling()

View File

@@ -49,14 +49,28 @@ const activeGenerations = computed(() => {
return [...bausteinLines, ...guideLines]
})
// Inline-Bestätigung statt confirm(): erster Klick scharfschalten („Sicher?"),
// zweiter Klick führt aus. Browser-Dialoge können unterdrückt sein (Firefox).
const pendingConfirm = ref(null)
let confirmTimer = null
function armOrRun(key, action) {
clearTimeout(confirmTimer)
if (pendingConfirm.value === key) {
pendingConfirm.value = null
action()
} else {
pendingConfirm.value = key
confirmTimer = setTimeout(() => { pendingConfirm.value = null }, 3000)
}
}
function confirmCancelBausteine() {
if (!confirm('Aktuellen Schritt abbrechen? Bisheriger Fortschritt bleibt erhalten.')) return
emit('cancelBausteine')
armOrRun('bausteine', () => emit('cancelBausteine'))
}
function confirmResetBausteine() {
if (!confirm('Gespeicherten Bausteine-Fortschritt löschen?')) return
emit('resetBausteine')
armOrRun('bausteine', () => emit('resetBausteine'))
}
function handleBausteinePlay() {
@@ -68,10 +82,12 @@ function handleBausteinePlay() {
}
function guideStatus(format) {
if (props.doneByFormat[format]) return 'done'
// Laufende Generierung hat Vorrang — sonst maskiert ein älterer fertiger
// Guide den Lauf und ▶ würde Duplikate starten.
const latest = props.latestByFormat[format]
if (!latest) return 'none'
if (latest.status === 'error') return 'none'
if (latest && (latest.status === 'generating' || latest.status === 'queued')) return latest.status
if (props.doneByFormat[format]) return 'done'
if (!latest || latest.status === 'error') return 'none'
return latest.status
}
@@ -116,15 +132,19 @@ function dismissError(format) {
}
function handleDelete(format) {
const guide = props.latestByFormat[format]
if (!guide) return
if (guide.status === 'generating' || guide.status === 'queued') {
if (!confirm('Generierung abbrechen?')) return
emit('cancelGuide', guide.id)
if (!props.latestByFormat[format]) return
armOrRun('fmt-' + format, () => {
// Alle laufenden Generierungen des Formats abbrechen (deckt auch Duplikate ab)
const running = props.allGuides.filter(
(g) => g.topic === props.selectedTopic && g.format === format
&& (g.status === 'generating' || g.status === 'queued'),
)
if (running.length) {
for (const g of running) emit('cancelGuide', g.id)
} else {
if (!confirm('Guide löschen?')) return
emit('deleteGuide', guide.id)
emit('deleteGuide', props.latestByFormat[format].id)
}
})
}
const newTopic = ref('')
@@ -137,13 +157,11 @@ function submit() {
}
function confirmDeleteTopic(topic) {
if (!confirm(`Thema "${topic}" und alle zugehörigen Guides löschen?`)) return
emit('deleteTopic', topic)
armOrRun('topic-' + topic, () => emit('deleteTopic', topic))
}
function confirmDeleteProject(name) {
if (!confirm(`Projekt "${name}" entfernen?\n\nAchtung: Der Quellordner ./projects/${name} wird gelöscht.`)) return
emit('deleteProject', name)
armOrRun('project-' + name, () => emit('deleteProject', name))
}
</script>
@@ -196,15 +214,17 @@ function confirmDeleteProject(name) {
<span
v-if="bausteineState === 'generating'"
class="format-x"
:class="{ armed: pendingConfirm === 'bausteine' }"
title="Aktuellen Schritt abbrechen (Fortschritt bleibt)"
@click.stop="confirmCancelBausteine"
>&times;</span>
>{{ pendingConfirm === 'bausteine' ? 'Sicher?' : '×' }}</span>
<span
v-else-if="bausteine.partial"
class="format-x"
:class="{ armed: pendingConfirm === 'bausteine' }"
title="Fortschritt löschen"
@click.stop="confirmResetBausteine"
>&times;</span>
>{{ pendingConfirm === 'bausteine' ? 'Sicher?' : '×' }}</span>
</div>
<div class="format-actions">
<template v-if="bausteineState !== 'generating'">
@@ -239,9 +259,10 @@ function confirmDeleteProject(name) {
<span
v-if="guideStatus(f.key) !== 'none'"
class="format-x"
:class="{ armed: pendingConfirm === 'fmt-' + f.key }"
@click.stop="handleDelete(f.key)"
:title="guideStatus(f.key) === 'generating' || guideStatus(f.key) === 'queued' ? 'Abbrechen' : 'Löschen'"
>&times;</span>
>{{ pendingConfirm === 'fmt-' + f.key ? 'Sicher?' : '×' }}</span>
</button>
<div class="format-actions">
<template v-if="guideStatus(f.key) !== 'generating' && guideStatus(f.key) !== 'queued'">
@@ -282,7 +303,12 @@ function confirmDeleteProject(name) {
@click="emit('select', t)"
>
<span>{{ t }}</span>
<button class="delete-topic" @click.stop="confirmDeleteTopic(t)" title="Löschen">&times;</button>
<button
class="delete-topic"
:class="{ armed: pendingConfirm === 'topic-' + t }"
@click.stop="confirmDeleteTopic(t)"
title="Thema und alle Guides löschen"
>{{ pendingConfirm === 'topic-' + t ? 'Sicher?' : '×' }}</button>
</li>
<template v-if="projects.length">
<li class="projects-divider">Projekte</li>
@@ -293,7 +319,12 @@ function confirmDeleteProject(name) {
@click="emit('select', p)"
>
<span>{{ p }}</span>
<button class="delete-topic" @click.stop="confirmDeleteProject(p)" title="Projekt entfernen">&times;</button>
<button
class="delete-topic"
:class="{ armed: pendingConfirm === 'project-' + p }"
@click.stop="confirmDeleteProject(p)"
title="Projekt entfernen (löscht ./projects-Ordner)"
>{{ pendingConfirm === 'project-' + p ? 'Sicher?' : '×' }}</button>
</li>
</template>
</ul>
@@ -578,6 +609,17 @@ function confirmDeleteProject(name) {
display: inline;
}
.format-x.armed,
.delete-topic.armed {
display: inline-block;
font-size: 0.7rem;
font-weight: 700;
background: var(--danger);
color: #fff;
border-radius: 4px;
padding: 2px 6px;
}
.fmt-done .format-name {
color: var(--success);
font-weight: 600;

View File

@@ -0,0 +1,17 @@
Sammle die Faktenbasis für einen OnePager — ein Einordnungs- und Entscheidungsdokument — zum Projekt "{topic}".
{source}
Erfasse gezielt diese Dimensionen:
1. Definition: Was ist "{topic}" in 12 Sätzen (Art des Projekts, Gegenstand)?
2. Problem: Welches Problem löst es, für wen ist es gedacht?
3. Abgrenzung: Was deckt das Projekt ab, was ausdrücklich nicht?
4. Einordnung: In welchem Kontext steht es (Umfeld, Abhängigkeiten, angrenzende Systeme/Themen)?
5. Anschauung: Ein typisches, konkretes Beispiel aus dem Projekt (zentraler Code-Flow bzw. Kerninhalt).
6. Fakten: Technologie/Format, Umfang (Dateien/Seiten/Module), Stand/Aktualität.
7. Einstieg: Wo fängt man an — wie startet man es bzw. was liest man zuerst?
Schreibe NUR die Markdown-Datei nach: {out_path}
Kompakt, faktenorientiert, mit Quelle (Dateipfad) pro Punkt. Nichts Erfundenes — nur was in den Projektdateien belegt ist. Die Datei ist die alleinige Faktenbasis für den OnePager.
{extra}