feat(stellungnahme): zentrales KI-Regelwerk aus stellungnahme_config
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 key→string 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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user