From 4376c403691562fe3d1226720f6caa1c2c0fc337 Mon Sep 17 00:00:00 2001 From: Peter Meier Date: Wed, 8 Jul 2026 13:56:16 +0200 Subject: [PATCH] feat(stellungnahme): zentrales KI-Regelwerk aus stellungnahme_config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/lib/block-types.ts | 2 ++ src/lib/blog-utils.ts | 15 +++++++++++++ src/lib/cms.ts | 21 +++++++++++++++++++ .../blocks/StellingnahmeGeneratorBlock.svelte | 11 +++++++--- src/lib/constants.ts | 3 +++ 5 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/lib/block-types.ts b/src/lib/block-types.ts index 5deefc3..33a1d72 100644 --- a/src/lib/block-types.ts +++ b/src/lib/block-types.ts @@ -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; } diff --git a/src/lib/blog-utils.ts b/src/lib/blog-utils.ts index 6e13d78..d4debfd 100644 --- a/src/lib/blog-utils.ts +++ b/src/lib/blog-utils.ts @@ -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; + // Zentrales Regelwerk anhängen (Fallback auf Hardcode-Default in der Komponente) + if (aiRules) block.aiRules = aiRules; + // Resolve windArea const windAreaRef = block.windArea; const windAreaSlug = diff --git a/src/lib/cms.ts b/src/lib/cms.ts index 3bbd962..49dc685 100644 --- a/src/lib/cms.ts +++ b/src/lib/cms.ts @@ -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 { + return getEntryBySlug( + "stellungnahme_config", + slug, + { locale: options?.locale }, + ); +} + /** * Normalised key→string map for `event.locals.translations`. Cached on the * raw `strings` object identity so the per-request `Object.entries` loop runs diff --git a/src/lib/components/blocks/StellingnahmeGeneratorBlock.svelte b/src/lib/components/blocks/StellingnahmeGeneratorBlock.svelte index 915a3aa..2224505 100644 --- a/src/lib/components/blocks/StellingnahmeGeneratorBlock.svelte +++ b/src/lib/components/blocks/StellingnahmeGeneratorBlock.svelte @@ -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} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index e02a504..c386c29 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -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";