Files
eventmap/js/ai/promptBuilder.js
dapelza 5e2197ec24 Phase 6: KI-Prompt-Generator
Neues Modul js/ai/promptBuilder.js: reine Funktion buildPrompt(), deckt
alle 10 Pflichtpunkte aus anforderung.md explizit ab (Region+Radius,
Kategorien, Alter, wichtige Eigenschaften, Anzahl Ergebnisse, keine
erfundenen Informationen, unsichere Angaben kennzeichnen, ausschliesslich
gueltiges JSON, genaue Koordinaten, direkte Importierbarkeit) und bettet
das exakte JSON-Zielschema aus anforderung.md unveraendert ein.

UI (js/ui/promptGenerator.js): Modal mit allen Eingabefeldern aus der
Anforderung (Region, Radius, Kategorien, Altersgruppe, Indoor/Outdoor,
Kostenrahmen, max. Ergebnisse, besondere Anforderungen als Checkboxen +
Freitext, Ausgabesprache), Live-Vorschau des generierten Prompts,
Copy-to-Clipboard mit execCommand-Fallback (js/utils/clipboard.js) und
Speichern als Textdatei (js/utils/download.js). Neuer Toolbar-Button
oeffnet den Generator.

JSON-Import der KI-Antwort ist bewusst nicht Teil dieser Phase, sondern
folgt in Phase 7. docs/plan.md entsprechend fortgeschrieben.
2026-07-24 12:32:37 +02:00

238 lines
9.8 KiB
JavaScript

// Reine Funktion zur Erzeugung des KI-Prompt-Textes für den
// KI-Prompt-Generator (Phase 6, siehe anforderung.md Abschnitt
// "KI-Prompt-Generator"). Keine UI-, keine Store-Zugriffe: nimmt
// ausschließlich Formularwerte entgegen und liefert einen fertigen
// Prompt-String zurück (js/ui/promptGenerator.js ruft diese Funktion bei
// jeder Formularänderung neu auf).
//
// Design-Entscheidung Sprache: Der Anweisungstext des Prompts selbst wird
// immer auf Deutsch verfasst (Zielgruppe der App ist deutschsprachig, siehe
// restliche UI-Texte). Das Formularfeld "Sprache der Ausgabe" steuert
// ausschließlich, in welcher Sprache die KI ihre Ergebnistexte
// (name/shortDescription/description) verfassen soll - das wird im Prompt
// explizit als eigene Anweisung an die KI übergeben.
(function () {
// 1:1 aus anforderung.md, Abschnitt "Vorgabe für das KI-JSON" übernommen,
// damit die KI exakt das Zielschema vor Augen hat, das später (Phase 7)
// in die Anwendung importiert werden kann.
const JSON_TEMPLATE = `{
"version": "1.0",
"generatedAt": "2026-01-01T12:00:00Z",
"region": "Beispielregion",
"places": [
{
"name": "Beispiel-Ausflugsziel",
"category": "Spielplatz",
"shortDescription": "Kurze Beschreibung des Ausflugsziels.",
"description": "Ausführliche Beschreibung des Ausflugsziels und seiner Besonderheiten.",
"latitude": 52.520008,
"longitude": 13.404954,
"address": "Beispielstraße 1",
"city": "Beispielstadt",
"region": "Beispielregion",
"ageRecommendation": {
"min": 2,
"max": 12
},
"cost": {
"type": "free",
"description": "Kostenlos"
},
"openingHours": "Täglich von 08:00 bis 20:00 Uhr",
"durationMinutes": 120,
"facilities": {
"strollerAccessible": true,
"wheelchairAccessible": false,
"toilets": true,
"parking": true,
"restaurant": false
},
"environment": "outdoor",
"website": "",
"image": "",
"rating": null,
"tags": [
"familienfreundlich",
"kostenlos",
"Spielplatz"
],
"source": "",
"sourceVerified": false
}
]
}`;
const ENVIRONMENT_LABELS = {
indoor: 'ausschließlich Indoor-Ziele (drinnen)',
outdoor: 'ausschließlich Outdoor-Ziele (draußen)',
both: 'sowohl Indoor- als auch Outdoor-Ziele',
};
const COST_RANGE_LABELS = {
any: 'Der Kostenrahmen spielt keine Rolle.',
free: 'Bitte ausschließlich kostenlose Ausflugsziele vorschlagen.',
low: 'Bitte bevorzugt kostenlose oder preisgünstige Ausflugsziele (z.B. geringer Eintrittspreis) vorschlagen.',
};
const LANGUAGE_LABELS = {
de: 'Deutsch',
en: 'Englisch (English)',
};
function formatRegionAndRadius(region, radiusKm) {
const trimmedRegion = (region || '').trim() || '(keine Region angegeben)';
if (radiusKm !== null && radiusKm !== undefined && Number.isFinite(radiusKm) && radiusKm > 0) {
return (
`Recherchiere ausschließlich familienfreundliche Ausflugsziele im folgenden Gebiet: ` +
`"${trimmedRegion}", innerhalb eines Umkreises von maximal ${radiusKm} km um dieses Gebiet.`
);
}
return (
`Recherchiere ausschließlich familienfreundliche Ausflugsziele im folgenden Gebiet: ` +
`"${trimmedRegion}". Es ist kein fester Radius vorgegeben - bleibe sinnvoll im Umfeld dieser Region.`
);
}
function formatCategories(categoryLabels) {
const { CATEGORIES } = EventMap.config;
const allLabels = CATEGORIES.map((cat) => cat.label);
const validValuesHint = `Verwende im JSON-Feld "category" ausschließlich einen der folgenden Werte: ${allLabels.join(', ')}.`;
if (Array.isArray(categoryLabels) && categoryLabels.length > 0) {
return `Berücksichtige ausschließlich Ausflugsziele der folgenden Kategorien: ${categoryLabels.join(', ')}. ${validValuesHint}`;
}
return `Es ist keine Kategorie eingeschränkt - alle familienfreundlichen Kategorien aus der folgenden Liste sind willkommen. ${validValuesHint}`;
}
function formatAgeRange(ageMin, ageMax) {
const hasMin = ageMin !== null && ageMin !== undefined && Number.isFinite(ageMin);
const hasMax = ageMax !== null && ageMax !== undefined && Number.isFinite(ageMax);
if (hasMin && hasMax) {
return `Die Ausflugsziele sollen für Kinder im Alter von ${ageMin} bis ${ageMax} Jahren geeignet sein.`;
}
if (hasMin) {
return `Die Ausflugsziele sollen für Kinder ab ${ageMin} Jahren geeignet sein.`;
}
if (hasMax) {
return `Die Ausflugsziele sollen für Kinder bis ${ageMax} Jahren geeignet sein.`;
}
return 'Es ist keine bestimmte Altersgruppe vorgegeben - die Ausflugsziele sollen grundsätzlich für Familien mit Kindern geeignet sein.';
}
function formatFeatures(specialRequirements, additionalRequirements, environmentKey, costRangeKey, costDetails) {
const lines = [];
lines.push(`Umgebung: ${ENVIRONMENT_LABELS[environmentKey] || ENVIRONMENT_LABELS.both}.`);
lines.push(COST_RANGE_LABELS[costRangeKey] || COST_RANGE_LABELS.any);
if ((costDetails || '').trim() !== '') {
lines.push(`Zusätzliche Angabe zum Kostenrahmen: ${costDetails.trim()}`);
}
if (Array.isArray(specialRequirements) && specialRequirements.length > 0) {
lines.push(
`Folgende Eigenschaften sind besonders wichtig: ${specialRequirements.join(', ')}.`
);
}
if ((additionalRequirements || '').trim() !== '') {
lines.push(`Weitere Anforderungen: ${additionalRequirements.trim()}`);
}
return lines.join('\n');
}
function formatMaxResults(maxResults) {
const n = Number.isFinite(maxResults) && maxResults > 0 ? Math.round(maxResults) : 10;
return (
`Liefere maximal ${n} Ausflugsziele. Liefere lieber weniger, aber dafür ausschließlich ` +
`korrekte und gut recherchierte Einträge, statt die Zielzahl mit unsicheren oder erfundenen Orten aufzufüllen.`
);
}
function formatLanguage(outputLanguage) {
const label = LANGUAGE_LABELS[outputLanguage] || LANGUAGE_LABELS.de;
return `Verfasse die Textfelder "name", "shortDescription" und "description" in folgender Sprache: ${label}.`;
}
/**
* Baut aus den Formularwerten des Prompt-Generators einen vollständigen,
* kopierbaren Prompt-Text.
*
* @param {object} values
* @param {string} values.region Region/Ort/Suchgebiet (Pflichtfeld)
* @param {number|null} values.radiusKm Maximaler Radius in km
* @param {string[]} values.categoryLabels Gewünschte Kategorien (Klartext-Labels)
* @param {number|null} values.ageMin Altersgruppe ab
* @param {number|null} values.ageMax Altersgruppe bis
* @param {'indoor'|'outdoor'|'both'} values.environment
* @param {'any'|'free'|'low'} values.costRange
* @param {string} values.costDetails Freitext-Zusatz zum Kostenrahmen
* @param {number} values.maxResults Maximale Anzahl an Ergebnissen
* @param {string[]} values.specialRequirements Ausgewählte besondere Anforderungen
* @param {string} values.additionalRequirements Freitext für weitere Anforderungen
* @param {'de'|'en'} values.outputLanguage Sprache der KI-Ausgabetexte
* @returns {string}
*/
function buildPrompt(values) {
const v = values || {};
const sections = [
'Du bist ein sorgfältiger Recherche-Assistent für familienfreundliche Ausflugsziele mit Kindern.',
'Recherchiere passende Ausflugsziele anhand der folgenden Vorgaben und liefere das Ergebnis ' +
'ausschließlich im weiter unten vorgegebenen JSON-Format.',
'',
'1. Region und Suchradius',
formatRegionAndRadius(v.region, v.radiusKm),
'',
'2. Gewünschte Kategorien',
formatCategories(v.categoryLabels),
'',
'3. Altersgruppe der Kinder',
formatAgeRange(v.ageMin, v.ageMax),
'',
'4. Wichtige Eigenschaften und besondere Anforderungen',
formatFeatures(v.specialRequirements, v.additionalRequirements, v.environment, v.costRange, v.costDetails),
'',
'5. Anzahl der Ergebnisse',
formatMaxResults(v.maxResults),
'',
'6. Keine erfundenen Informationen',
'Gib ausschließlich Ausflugsziele an, die dir tatsächlich bekannt sind bzw. die du recherchieren ' +
'kannst. Erfinde unter keinen Umständen Orte, Adressen, Koordinaten oder sonstige Angaben.',
'',
'7. Unsichere Angaben kennzeichnen',
'Falls du dir bei einzelnen Angaben (z.B. genauen Öffnungszeiten, Koordinaten oder Kosten) nicht ' +
'vollständig sicher bist, kennzeichne dies deutlich: setze in diesem Fall das Feld "sourceVerified" ' +
'auf false und ergänze im Feld "description" einen kurzen Hinweis wie "Angabe ungeprüft".',
'',
'8. Ausgabeformat',
'Antworte ausschließlich mit einem einzigen, gültigen JSON-Objekt - ohne einleitenden oder ' +
'abschließenden Fließtext, ohne Markdown-Codeblock-Auszeichnung außerhalb des JSON und ohne ' +
'Kommentare innerhalb des JSON.',
'',
'9. Genaue Koordinaten',
'Liefere für jedes Ausflugsziel möglichst genaue Koordinaten (Felder "latitude"/"longitude" als ' +
'Dezimalgrad mit mindestens 5 Nachkommastellen), damit der Ort exakt auf einer Karte lokalisiert werden kann.',
'',
'10. Direkte Importierbarkeit',
'Das JSON muss exakt dem unten stehenden Zielformat entsprechen (identische Feldnamen, Struktur ' +
'und Datentypen), damit es ohne manuelle Anpassung direkt in die Anwendung importiert werden kann.',
'',
'Sprache der Ausgabetexte',
formatLanguage(v.outputLanguage),
'',
'Zielformat (bitte exakt einhalten, Beispielwerte durch recherchierte Daten ersetzen)',
'```json',
JSON_TEMPLATE,
'```',
];
return sections.join('\n');
}
EventMap.ai.buildPrompt = buildPrompt;
EventMap.ai.JSON_TEMPLATE = JSON_TEMPLATE;
})();