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.
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
// 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;
|
||||
})();
|
||||
Reference in New Issue
Block a user