feat(stellungnahme): zentrales KI-Regelwerk aus stellungnahme_config
Deploy / verify (push) Successful in 1m21s
Deploy / deploy (push) Successful in 2m13s

Die AI-Rewrite-Regeln (DEFAULT_AI_RULES) waren im Block hardcodiert und
hätten pro Generator (40 Instanzen) dupliziert werden müssen, um im CMS
editierbar zu sein. Stattdessen ein Singleton stellungnahme_config
(Slug "default") mit Feld aiRules: der Resolver lädt es einmal und hängt
block.aiRules an alle Generator-Blöcke. Komponente seedet daraus, mit
DEFAULT_AI_RULES als Code-Fallback; Reset-Button setzt auf den CMS-Wert.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-07-08 13:56:16 +02:00
parent 7379d5750c
commit 4376c40369
5 changed files with 49 additions and 3 deletions
+2
View File
@@ -524,5 +524,7 @@ export interface StellingnahmeGeneratorBlockData {
deadline?: string;
/** E-Mail der Planungsstelle — aktiviert Mail-Button. */
recipientEmail?: string;
/** Vom Resolver befüllt: zentrales KI-Regelwerk aus stellungnahme_config (Singleton). */
aiRules?: string;
layout?: BlockLayout;
}
+15
View File
@@ -6,11 +6,13 @@ import {
getTextFragmentBySlug,
getTextFragmentsByTag,
getEntryBySlug,
getStellungnahmeConfig,
getCalendarBySlug,
getCalendarItemBySlug,
getContacts,
getCalendarItems,
} from "./cms";
import { STELLUNGNAHME_CONFIG_SLUG } from "./constants";
import type { RowContentLayout } from "./block-types";
import type { CalendarItemData } from "./block-types";
import { ensureTransformedImage, extractCmsImageField, type CmsImageField } from "./rusty-image";
@@ -780,6 +782,7 @@ function isStellingnahmeGeneratorBlock(item: unknown): boolean {
/** Löst stellungnahme_generator-Blöcke auf:
* - windArea → vollständiges wind_area-Objekt mit aufgelösten stellungnahme_kriterien
* - allgemeineArgumenteTag → allgemeineArgumente[] via Tag-Filter
* - aiRules ← zentrale stellungnahme_config (Singleton), einmal geladen, an alle Blöcke gehängt
*/
export async function resolveStellingnahmeGeneratorBlocks(
layout: RowContentLayout | null | undefined,
@@ -790,12 +793,24 @@ export async function resolveStellingnahmeGeneratorBlocks(
layout.row2Content ?? [],
layout.row3Content ?? [],
];
// Zentrales Regelwerk nur laden, wenn überhaupt ein Generator-Block vorhanden ist.
const hasGenerator = rows.some(
(content) => Array.isArray(content) && content.some(isStellingnahmeGeneratorBlock),
);
const aiRules = hasGenerator
? (await getStellungnahmeConfig(STELLUNGNAHME_CONFIG_SLUG, { locale: "de" }))?.aiRules
: undefined;
for (const content of rows) {
if (!Array.isArray(content)) continue;
for (const item of content) {
if (!isStellingnahmeGeneratorBlock(item)) continue;
const block = item as Record<string, unknown>;
// Zentrales Regelwerk anhängen (Fallback auf Hardcode-Default in der Komponente)
if (aiRules) block.aiRules = aiRules;
// Resolve windArea
const windAreaRef = block.windArea;
const windAreaSlug =
+21
View File
@@ -947,6 +947,27 @@ export async function getTranslationBundleBySlug(
});
}
/** Zentrale Stellungnahme-Generator-Konfiguration (Singleton). */
export type StellungnahmeConfigEntry = {
aiRules?: string;
};
/**
* Lädt das zentrale Regelwerk für die Stellungnahme-Generatoren
* (GET /api/content/stellungnahme_config/:slug). Einmal definiert, von allen
* 40 Generator-Blöcken genutzt statt pro Block dupliziert.
*/
export async function getStellungnahmeConfig(
slug: string,
options?: { locale?: string },
): Promise<StellungnahmeConfigEntry | null> {
return getEntryBySlug<StellungnahmeConfigEntry>(
"stellungnahme_config",
slug,
{ locale: options?.locale },
);
}
/**
* Normalised keystring map for `event.locals.translations`. Cached on the
* raw `strings` object identity so the per-request `Object.entries` loop runs
@@ -458,7 +458,12 @@ ${"=".repeat(64)}`;
5. Wenn der Verfasser persönliche Betroffenheit angegeben hat, wird sie konkret und ich-bezogen in einen passenden Absatz des Hauptteils eingewoben, nicht als separater Block angehängt. Ohne Angabe verzichtest du auf erfundene persönliche Bezüge.
6. Quellen, Studien, Gesetzesnormen, Aktenzeichen und Fundstellen werden so konkret wie möglich direkt im Fließtext benannt (z.B. "§ 35 BauGB", "TA Lärm Nr. 6.1", Studientitel mit Jahr/Autor). Wo die Vorlage eine Quelle nennt, übernimmst du sie ausdrücklich; vage Formulierungen wie "Studien zeigen" ohne Beleg vermeidest du.
7. KEINE Verweise auf Anlagen, Anhänge, Beilagen oder externe/beigefügte Dokumente ("siehe Anlage", "im Anhang", "wie beigefügt", "Anlage 1"). Es wird NICHTS beigefügt: Jeder Beleg, jede Quelle und jeder Verweis muss vollständig im Dokument selbst ausgeschrieben stehen, sodass der Brief ohne weitere Unterlagen verständlich und prüfbar ist.`;
let aiRules = $state(DEFAULT_AI_RULES);
// Baseline = zentrales Regelwerk aus dem CMS (stellungnahme_config), Fallback = Hardcode-Default.
const baselineAiRules =
typeof block.aiRules === "string" && block.aiRules.trim().length > 0
? block.aiRules
: DEFAULT_AI_RULES;
let aiRules = $state(baselineAiRules);
// Badges für "Brief im Detail anpassen"-Accordion
const tweakBadges = $derived(
@@ -1407,8 +1412,8 @@ ${outputText}
<button
type="button"
class="text-[10px] text-stein-500 underline-offset-2 hover:text-stein-700 hover:underline"
onclick={() => (aiRules = DEFAULT_AI_RULES)}
disabled={aiRules.trim() === DEFAULT_AI_RULES.trim()}
onclick={() => (aiRules = baselineAiRules)}
disabled={aiRules.trim() === baselineAiRules.trim()}
>Auf Standard zurücksetzen</button>
</div>
</div>
+3
View File
@@ -48,3 +48,6 @@ export const SITE_NAME = "Windwiderstand";
/** Slug des Translation-Bundles im CMS (content/de/translation_bundle/{slug}.json5). Alle UI-Texte aus diesem Bundle. */
export const TRANSLATION_BUNDLE_SLUG = "app";
/** Slug der zentralen Stellungnahme-Generator-Konfiguration (Singleton). Regelwerk für alle Generatoren, statt 40× dupliziert. */
export const STELLUNGNAHME_CONFIG_SLUG = "default";