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"
|
recherche_path = content_path.parent / f"{content_path.stem}.recherche.md"
|
||||||
fragment_paths.append(recherche_path)
|
fragment_paths.append(recherche_path)
|
||||||
recherche_path.unlink(missing_ok=True)
|
recherche_path.unlink(missing_ok=True)
|
||||||
|
# Projekte bekommen eigene Recherche-Dimensionen — Produkt-Fragen
|
||||||
|
# (Version, Lizenz, Alternativen) laufen dort ins Leere.
|
||||||
if project:
|
if project:
|
||||||
source = _prompt("OnePager-Quelle-Projekt", project=project)
|
source = _prompt("OnePager-Quelle-Projekt", project=project)
|
||||||
|
recherche_template = "OnePager-Recherche-Projekt"
|
||||||
else:
|
else:
|
||||||
source = _prompt("OnePager-Quelle-Thema", topic=topic)
|
source = _prompt("OnePager-Quelle-Thema", topic=topic)
|
||||||
|
recherche_template = "OnePager-Recherche"
|
||||||
slots = [{
|
slots = [{
|
||||||
"key": f"{guide_id}-recherche",
|
"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",
|
"role": "quick", "capabilities": "files" if project else "full",
|
||||||
"payload": (lambda result: recherche_path.read_text(encoding="utf-8") if recherche_path.exists() else None),
|
"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):
|
async def create(req: GuideCreateRequest):
|
||||||
if req.format != "OnePager" and not bausteine_path(req.topic.strip()).exists():
|
if req.format != "OnePager" and not bausteine_path(req.topic.strip()).exists():
|
||||||
raise HTTPException(400, "Erst Bausteine erstellen")
|
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())
|
await create_topic(req.topic.strip())
|
||||||
now = datetime.now(timezone.utc).isoformat()
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
guide = {
|
guide = {
|
||||||
|
|||||||
@@ -189,6 +189,12 @@ async function handleBausteineClick({ instructions }) {
|
|||||||
|
|
||||||
async function handleFormatClick({ format, instructions }) {
|
async function handleFormatClick({ format, instructions }) {
|
||||||
if (!selectedTopic.value) return
|
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 apiCreate(selectedTopic.value, format, instructions, provider.value)
|
||||||
await loadGuides()
|
await loadGuides()
|
||||||
startPolling()
|
startPolling()
|
||||||
|
|||||||
@@ -49,14 +49,28 @@ const activeGenerations = computed(() => {
|
|||||||
return [...bausteinLines, ...guideLines]
|
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() {
|
function confirmCancelBausteine() {
|
||||||
if (!confirm('Aktuellen Schritt abbrechen? Bisheriger Fortschritt bleibt erhalten.')) return
|
armOrRun('bausteine', () => emit('cancelBausteine'))
|
||||||
emit('cancelBausteine')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmResetBausteine() {
|
function confirmResetBausteine() {
|
||||||
if (!confirm('Gespeicherten Bausteine-Fortschritt löschen?')) return
|
armOrRun('bausteine', () => emit('resetBausteine'))
|
||||||
emit('resetBausteine')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBausteinePlay() {
|
function handleBausteinePlay() {
|
||||||
@@ -68,10 +82,12 @@ function handleBausteinePlay() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function guideStatus(format) {
|
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]
|
const latest = props.latestByFormat[format]
|
||||||
if (!latest) return 'none'
|
if (latest && (latest.status === 'generating' || latest.status === 'queued')) return latest.status
|
||||||
if (latest.status === 'error') return 'none'
|
if (props.doneByFormat[format]) return 'done'
|
||||||
|
if (!latest || latest.status === 'error') return 'none'
|
||||||
return latest.status
|
return latest.status
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,15 +132,19 @@ function dismissError(format) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleDelete(format) {
|
function handleDelete(format) {
|
||||||
const guide = props.latestByFormat[format]
|
if (!props.latestByFormat[format]) return
|
||||||
if (!guide) return
|
armOrRun('fmt-' + format, () => {
|
||||||
if (guide.status === 'generating' || guide.status === 'queued') {
|
// Alle laufenden Generierungen des Formats abbrechen (deckt auch Duplikate ab)
|
||||||
if (!confirm('Generierung abbrechen?')) return
|
const running = props.allGuides.filter(
|
||||||
emit('cancelGuide', guide.id)
|
(g) => g.topic === props.selectedTopic && g.format === format
|
||||||
} else {
|
&& (g.status === 'generating' || g.status === 'queued'),
|
||||||
if (!confirm('Guide löschen?')) return
|
)
|
||||||
emit('deleteGuide', guide.id)
|
if (running.length) {
|
||||||
}
|
for (const g of running) emit('cancelGuide', g.id)
|
||||||
|
} else {
|
||||||
|
emit('deleteGuide', props.latestByFormat[format].id)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTopic = ref('')
|
const newTopic = ref('')
|
||||||
@@ -137,13 +157,11 @@ function submit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function confirmDeleteTopic(topic) {
|
function confirmDeleteTopic(topic) {
|
||||||
if (!confirm(`Thema "${topic}" und alle zugehörigen Guides löschen?`)) return
|
armOrRun('topic-' + topic, () => emit('deleteTopic', topic))
|
||||||
emit('deleteTopic', topic)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmDeleteProject(name) {
|
function confirmDeleteProject(name) {
|
||||||
if (!confirm(`Projekt "${name}" entfernen?\n\nAchtung: Der Quellordner ./projects/${name} wird gelöscht.`)) return
|
armOrRun('project-' + name, () => emit('deleteProject', name))
|
||||||
emit('deleteProject', name)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -196,15 +214,17 @@ function confirmDeleteProject(name) {
|
|||||||
<span
|
<span
|
||||||
v-if="bausteineState === 'generating'"
|
v-if="bausteineState === 'generating'"
|
||||||
class="format-x"
|
class="format-x"
|
||||||
|
:class="{ armed: pendingConfirm === 'bausteine' }"
|
||||||
title="Aktuellen Schritt abbrechen (Fortschritt bleibt)"
|
title="Aktuellen Schritt abbrechen (Fortschritt bleibt)"
|
||||||
@click.stop="confirmCancelBausteine"
|
@click.stop="confirmCancelBausteine"
|
||||||
>×</span>
|
>{{ pendingConfirm === 'bausteine' ? 'Sicher?' : '×' }}</span>
|
||||||
<span
|
<span
|
||||||
v-else-if="bausteine.partial"
|
v-else-if="bausteine.partial"
|
||||||
class="format-x"
|
class="format-x"
|
||||||
|
:class="{ armed: pendingConfirm === 'bausteine' }"
|
||||||
title="Fortschritt löschen"
|
title="Fortschritt löschen"
|
||||||
@click.stop="confirmResetBausteine"
|
@click.stop="confirmResetBausteine"
|
||||||
>×</span>
|
>{{ pendingConfirm === 'bausteine' ? 'Sicher?' : '×' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="format-actions">
|
<div class="format-actions">
|
||||||
<template v-if="bausteineState !== 'generating'">
|
<template v-if="bausteineState !== 'generating'">
|
||||||
@@ -239,9 +259,10 @@ function confirmDeleteProject(name) {
|
|||||||
<span
|
<span
|
||||||
v-if="guideStatus(f.key) !== 'none'"
|
v-if="guideStatus(f.key) !== 'none'"
|
||||||
class="format-x"
|
class="format-x"
|
||||||
|
:class="{ armed: pendingConfirm === 'fmt-' + f.key }"
|
||||||
@click.stop="handleDelete(f.key)"
|
@click.stop="handleDelete(f.key)"
|
||||||
:title="guideStatus(f.key) === 'generating' || guideStatus(f.key) === 'queued' ? 'Abbrechen' : 'Löschen'"
|
:title="guideStatus(f.key) === 'generating' || guideStatus(f.key) === 'queued' ? 'Abbrechen' : 'Löschen'"
|
||||||
>×</span>
|
>{{ pendingConfirm === 'fmt-' + f.key ? 'Sicher?' : '×' }}</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="format-actions">
|
<div class="format-actions">
|
||||||
<template v-if="guideStatus(f.key) !== 'generating' && guideStatus(f.key) !== 'queued'">
|
<template v-if="guideStatus(f.key) !== 'generating' && guideStatus(f.key) !== 'queued'">
|
||||||
@@ -282,7 +303,12 @@ function confirmDeleteProject(name) {
|
|||||||
@click="emit('select', t)"
|
@click="emit('select', t)"
|
||||||
>
|
>
|
||||||
<span>{{ t }}</span>
|
<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>
|
</li>
|
||||||
<template v-if="projects.length">
|
<template v-if="projects.length">
|
||||||
<li class="projects-divider">Projekte</li>
|
<li class="projects-divider">Projekte</li>
|
||||||
@@ -293,7 +319,12 @@ function confirmDeleteProject(name) {
|
|||||||
@click="emit('select', p)"
|
@click="emit('select', p)"
|
||||||
>
|
>
|
||||||
<span>{{ p }}</span>
|
<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>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -578,6 +609,17 @@ function confirmDeleteProject(name) {
|
|||||||
display: inline;
|
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 {
|
.fmt-done .format-name {
|
||||||
color: var(--success);
|
color: var(--success);
|
||||||
font-weight: 600;
|
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