update
This commit is contained in:
@@ -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),
|
||||
}]
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
} else {
|
||||
if (!confirm('Guide löschen?')) return
|
||||
emit('deleteGuide', 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 {
|
||||
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"
|
||||
>×</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"
|
||||
>×</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'"
|
||||
>×</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">×</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">×</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;
|
||||
|
||||
17
templates/Prompt/OnePager-Recherche-Projekt.md
Normal file
17
templates/Prompt/OnePager-Recherche-Projekt.md
Normal 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 1–2 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}
|
||||
Reference in New Issue
Block a user