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,155 @@
|
|||||||
|
/* KI-Prompt-Generator (Modal), Phase 6: Vollbild auf Mobile, zentriertes
|
||||||
|
breites Panel ab 1024px (siehe Breakpoint in css/responsive.css). Struktur
|
||||||
|
wird von js/ui/promptGenerator.js in #prompt-generator-modal eingehängt.
|
||||||
|
Grundgerüst analog zu css/form.css/css/detail.css; das Eingabeformular
|
||||||
|
selbst nutzt bewusst dieselben Klassen wie das Ausflugsziel-Formular
|
||||||
|
(.place-form, .place-form__section, .form-field, ...) statt sie zu
|
||||||
|
duplizieren. */
|
||||||
|
|
||||||
|
.prompt-generator-modal {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 2200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-generator-modal[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-generator-modal__backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-generator-modal__panel {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--color-surface);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: var(--space-lg);
|
||||||
|
padding-top: calc(var(--space-lg) + 36px);
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-generator-modal__close {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--space-sm);
|
||||||
|
right: var(--space-sm);
|
||||||
|
z-index: 1;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(0, 0, 0, 0.55);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-generator__title {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-generator__intro {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-generator__layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-generator__form.place-form {
|
||||||
|
padding: 0;
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-generator__location-btn[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-generator__output {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-generator__output-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 320px;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: var(--space-sm);
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
background: var(--color-background);
|
||||||
|
color: var(--color-text);
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-generator__output-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm) var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-generator__status {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-generator__instructions {
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
padding-top: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-generator__instructions-list {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.25em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.prompt-generator-modal__panel {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: min(1080px, 95vw);
|
||||||
|
height: min(90vh, 980px);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-generator__layout {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-generator__form.place-form,
|
||||||
|
.prompt-generator__output {
|
||||||
|
flex: 1 1 50%;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-generator__output-textarea {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
-1
@@ -160,9 +160,28 @@ Seed-Daten dadurch synchron direkt aus dem globalen Namespace, ganz ohne
|
|||||||
| 3 | Formular zum manuellen Anlegen/Bearbeiten (Validierung, Koordinatenprüfung) | ✅ umgesetzt |
|
| 3 | Formular zum manuellen Anlegen/Bearbeiten (Validierung, Koordinatenprüfung) | ✅ umgesetzt |
|
||||||
| 4 | Filter- und Suchfunktion (Kategorie, Entfernung, Alter, Kosten, Indoor/Outdoor, Ausstattung, Bewertung, Region) inkl. Anwendung auf Karte + Liste | ✅ umgesetzt |
|
| 4 | Filter- und Suchfunktion (Kategorie, Entfernung, Alter, Kosten, Indoor/Outdoor, Ausstattung, Bewertung, Region) inkl. Anwendung auf Karte + Liste | ✅ umgesetzt |
|
||||||
| 5 | Route/Navigation-Integration | ✅ bereits in Phase 2 erledigt (OSM-Directions-Button in der Detailansicht) |
|
| 5 | Route/Navigation-Integration | ✅ bereits in Phase 2 erledigt (OSM-Directions-Button in der Detailansicht) |
|
||||||
| 6 | KI-Prompt-Generator (Eingabeformular, Promptgenerierung, Copy/Download) | offen |
|
| 6 | KI-Prompt-Generator (Eingabeformular, Promptgenerierung, Copy/Download) | ✅ umgesetzt |
|
||||||
| 7 | KI-JSON-Import (Validierung, Vorschau, Übernahme ins LocalStorage) + Backup-Export/Import + Politur | offen |
|
| 7 | KI-JSON-Import (Validierung, Vorschau, Übernahme ins LocalStorage) + Backup-Export/Import + Politur | offen |
|
||||||
|
|
||||||
|
## KI-Prompt-Generator (Phase 6)
|
||||||
|
|
||||||
|
- `js/ai/promptBuilder.js`: reine Funktion `buildPrompt(formValues)`, deckt alle
|
||||||
|
10 Pflichtpunkte aus `anforderung.md` explizit nummeriert ab und bettet das
|
||||||
|
exakte JSON-Zielschema aus `anforderung.md` ("Vorgabe für das KI-JSON")
|
||||||
|
1:1 als Konstante `JSON_TEMPLATE` ein (per Skript byte-genau abgeglichen).
|
||||||
|
- Der Anweisungstext des Prompts ist immer Deutsch; das Formularfeld "Sprache
|
||||||
|
der Ausgabe" steuert separat, in welcher Sprache die KI ihre `name`/
|
||||||
|
`shortDescription`/`description`-Texte verfassen soll.
|
||||||
|
- `js/ui/promptGenerator.js`: Modal mit Live-Vorschau (`<textarea readonly>`,
|
||||||
|
aktualisiert sich bei jeder Eingabe), Buttons "In Zwischenablage kopieren"
|
||||||
|
(`js/utils/clipboard.js`, Clipboard API + `execCommand`-Fallback für
|
||||||
|
`file://`-Einschränkungen) und "Als Textdatei speichern"
|
||||||
|
(`js/utils/download.js`, clientseitiger Blob-Download).
|
||||||
|
- Einstiegspunkt: Button in der Karten-Toolbar, öffnet
|
||||||
|
`EventMap.ui.openPromptGenerator()`.
|
||||||
|
- Import der KI-Antwort (JSON zurück in die App) ist bewusst nicht Teil
|
||||||
|
dieser Phase, sondern folgt in Phase 7.
|
||||||
|
|
||||||
## Filter- und Suchfunktion (Phase 4)
|
## Filter- und Suchfunktion (Phase 4)
|
||||||
|
|
||||||
- `js/filters/filterEngine.js`: reine Funktion `applyFilters(places, filters, userLocation)`
|
- `js/filters/filterEngine.js`: reine Funktion `applyFilters(places, filters, userLocation)`
|
||||||
|
|||||||
+14
@@ -16,6 +16,7 @@
|
|||||||
<link rel="stylesheet" href="css/detail.css" />
|
<link rel="stylesheet" href="css/detail.css" />
|
||||||
<link rel="stylesheet" href="css/form.css" />
|
<link rel="stylesheet" href="css/form.css" />
|
||||||
<link rel="stylesheet" href="css/filters.css" />
|
<link rel="stylesheet" href="css/filters.css" />
|
||||||
|
<link rel="stylesheet" href="css/promptGenerator.css" />
|
||||||
<link rel="stylesheet" href="css/responsive.css" />
|
<link rel="stylesheet" href="css/responsive.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -35,6 +36,9 @@
|
|||||||
<button id="add-place-btn" type="button" class="btn btn--primary">
|
<button id="add-place-btn" type="button" class="btn btn--primary">
|
||||||
+ Neues Ausflugsziel hinzufügen
|
+ Neues Ausflugsziel hinzufügen
|
||||||
</button>
|
</button>
|
||||||
|
<button id="open-prompt-generator-btn" type="button" class="btn btn--secondary">
|
||||||
|
🤖 Neue Ausflugsziele mit KI erstellen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="map" class="map-panel__map">
|
<div id="map" class="map-panel__map">
|
||||||
<p class="map-panel__loading">Karte wird geladen…</p>
|
<p class="map-panel__loading">Karte wird geladen…</p>
|
||||||
@@ -53,6 +57,7 @@
|
|||||||
|
|
||||||
<div id="detail-modal" class="detail-modal" hidden></div>
|
<div id="detail-modal" class="detail-modal" hidden></div>
|
||||||
<div id="place-form-modal" class="place-form-modal" hidden></div>
|
<div id="place-form-modal" class="place-form-modal" hidden></div>
|
||||||
|
<div id="prompt-generator-modal" class="prompt-generator-modal" hidden></div>
|
||||||
|
|
||||||
<script src="vendor/leaflet/leaflet.js"></script>
|
<script src="vendor/leaflet/leaflet.js"></script>
|
||||||
<script src="vendor/leaflet.markercluster/leaflet.markercluster.js"></script>
|
<script src="vendor/leaflet.markercluster/leaflet.markercluster.js"></script>
|
||||||
@@ -64,6 +69,8 @@
|
|||||||
<script src="js/utils/dom.js"></script>
|
<script src="js/utils/dom.js"></script>
|
||||||
<script src="js/utils/geo.js"></script>
|
<script src="js/utils/geo.js"></script>
|
||||||
<script src="js/utils/id.js"></script>
|
<script src="js/utils/id.js"></script>
|
||||||
|
<script src="js/utils/clipboard.js"></script>
|
||||||
|
<script src="js/utils/download.js"></script>
|
||||||
<script src="js/filters/filterEngine.js"></script>
|
<script src="js/filters/filterEngine.js"></script>
|
||||||
<script src="data/seed.js"></script>
|
<script src="data/seed.js"></script>
|
||||||
<script src="js/config/constants.js"></script>
|
<script src="js/config/constants.js"></script>
|
||||||
@@ -80,6 +87,13 @@
|
|||||||
<script src="js/ui/detail.js"></script>
|
<script src="js/ui/detail.js"></script>
|
||||||
<script src="js/ui/form.js"></script>
|
<script src="js/ui/form.js"></script>
|
||||||
<script src="js/filters/filterPanel.js"></script>
|
<script src="js/filters/filterPanel.js"></script>
|
||||||
|
|
||||||
|
<!-- KI-Prompt-Generator (Phase 6): eigener Bereich, da js/ai/promptBuilder.js
|
||||||
|
auf EventMap.config.CATEGORIES angewiesen ist und js/ui/promptGenerator.js
|
||||||
|
wiederum promptBuilder.js voraussetzt. -->
|
||||||
|
<script src="js/ai/promptBuilder.js"></script>
|
||||||
|
<script src="js/ui/promptGenerator.js"></script>
|
||||||
|
|
||||||
<script src="js/main.js"></script>
|
<script src="js/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
})();
|
||||||
+12
-1
@@ -3,7 +3,7 @@
|
|||||||
(function () {
|
(function () {
|
||||||
const { initStore } = EventMap.store;
|
const { initStore } = EventMap.store;
|
||||||
const { initMap, centerOnCoords, requestUserLocation } = EventMap.map;
|
const { initMap, centerOnCoords, requestUserLocation } = EventMap.map;
|
||||||
const { initList, initDetail, initForm, initFilterPanel } = EventMap.ui;
|
const { initList, initDetail, initForm, initFilterPanel, initPromptGenerator } = EventMap.ui;
|
||||||
|
|
||||||
function showFatalError(message) {
|
function showFatalError(message) {
|
||||||
const status = document.getElementById('app-status');
|
const status = document.getElementById('app-status');
|
||||||
@@ -48,6 +48,15 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupPromptGeneratorButton() {
|
||||||
|
const button = document.getElementById('open-prompt-generator-btn');
|
||||||
|
if (!button) return;
|
||||||
|
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
EventMap.ui.openPromptGenerator();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
await initStore();
|
await initStore();
|
||||||
initMap('map');
|
initMap('map');
|
||||||
@@ -55,8 +64,10 @@
|
|||||||
initList('place-list');
|
initList('place-list');
|
||||||
initDetail('detail-modal');
|
initDetail('detail-modal');
|
||||||
initForm('place-form-modal');
|
initForm('place-form-modal');
|
||||||
|
initPromptGenerator('prompt-generator-modal');
|
||||||
setupGeolocationButton();
|
setupGeolocationButton();
|
||||||
setupAddPlaceButton();
|
setupAddPlaceButton();
|
||||||
|
setupPromptGeneratorButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap().catch((err) => {
|
bootstrap().catch((err) => {
|
||||||
|
|||||||
@@ -0,0 +1,442 @@
|
|||||||
|
// KI-Prompt-Generator (Modal), Phase 6 (siehe anforderung.md Abschnitt
|
||||||
|
// "KI-Prompt-Generator"). Sammelt Formularwerte, übergibt sie an die reine
|
||||||
|
// Funktion EventMap.ai.buildPrompt() (js/ai/promptBuilder.js) und zeigt das
|
||||||
|
// Ergebnis live in einem <textarea readonly> an. Bietet zusätzlich Buttons
|
||||||
|
// zum Kopieren (Zwischenablage) und Speichern als Textdatei.
|
||||||
|
//
|
||||||
|
// WICHTIG: Diese Phase implementiert nur die Prompt-Erzeugung. Der spätere
|
||||||
|
// JSON-Import (Einlesen der KI-Antwort zurück in die App) ist NICHT Teil
|
||||||
|
// dieser Datei - das folgt in Phase 7.
|
||||||
|
//
|
||||||
|
// Aufbau/Muster analog zu js/ui/form.js (Modal mit Backdrop, Escape schließt,
|
||||||
|
// Fokus auf erstes Feld beim Öffnen). Rendering ausschließlich über
|
||||||
|
// textContent/DOM-APIs (el()), niemals innerHTML mit Nutzerdaten.
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
const { el, clearChildren } = EventMap.utils;
|
||||||
|
|
||||||
|
const SPECIAL_REQUIREMENTS = [
|
||||||
|
'Kinderwagen geeignet',
|
||||||
|
'Für Kleinkinder geeignet',
|
||||||
|
'Bei schlechtem Wetter geeignet',
|
||||||
|
'Kostenlos',
|
||||||
|
'Mit Tieren',
|
||||||
|
'Mit Spielplatz',
|
||||||
|
'Ruhige Umgebung',
|
||||||
|
'Barrierefrei',
|
||||||
|
'Für einen Tagesausflug geeignet',
|
||||||
|
];
|
||||||
|
|
||||||
|
const ENVIRONMENT_OPTIONS = [
|
||||||
|
{ value: 'both', label: 'Indoor & Outdoor' },
|
||||||
|
{ value: 'indoor', label: 'Nur Indoor' },
|
||||||
|
{ value: 'outdoor', label: 'Nur Outdoor' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const COST_RANGE_OPTIONS = [
|
||||||
|
{ value: 'any', label: 'Beliebig (keine Einschränkung)' },
|
||||||
|
{ value: 'low', label: 'Kostenlos oder günstig' },
|
||||||
|
{ value: 'free', label: 'Nur kostenlos' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const LANGUAGE_OPTIONS = [
|
||||||
|
{ value: 'de', label: 'Deutsch' },
|
||||||
|
{ value: 'en', label: 'Englisch' },
|
||||||
|
];
|
||||||
|
|
||||||
|
let container = null;
|
||||||
|
let refs = null;
|
||||||
|
|
||||||
|
function section(legendText, children) {
|
||||||
|
return el('fieldset', { className: 'place-form__section' }, [
|
||||||
|
el('legend', { className: 'place-form__section-label', text: legendText }),
|
||||||
|
...children,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function row(children) {
|
||||||
|
return el('div', { className: 'place-form__row' }, children);
|
||||||
|
}
|
||||||
|
|
||||||
|
function field(id, labelText, inputNode, { required = false } = {}) {
|
||||||
|
inputNode.id = id;
|
||||||
|
if (required) {
|
||||||
|
inputNode.setAttribute('required', '');
|
||||||
|
inputNode.setAttribute('aria-required', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelChildren = [labelText];
|
||||||
|
if (required) {
|
||||||
|
labelChildren.push(
|
||||||
|
el('span', { className: 'form-field__required', attrs: { 'aria-hidden': 'true' } }, [' *'])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return el('div', { className: 'form-field' }, [
|
||||||
|
el('label', { className: 'form-field__label', attrs: { for: id } }, labelChildren),
|
||||||
|
inputNode,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCategoryGroup() {
|
||||||
|
const { CATEGORIES } = EventMap.config;
|
||||||
|
refs.categoryInputs = [];
|
||||||
|
|
||||||
|
const options = CATEGORIES.map((cat) => {
|
||||||
|
const id = `pg-cat-${cat.id}`;
|
||||||
|
const input = el('input', {
|
||||||
|
attrs: { type: 'checkbox', id, value: cat.id },
|
||||||
|
on: { change: updateOutput },
|
||||||
|
});
|
||||||
|
refs.categoryInputs.push(input);
|
||||||
|
|
||||||
|
return el('label', { className: 'filter-panel__category-option', attrs: { for: id } }, [
|
||||||
|
input,
|
||||||
|
el('span', {
|
||||||
|
className: 'filter-panel__category-swatch',
|
||||||
|
attrs: { 'aria-hidden': 'true' },
|
||||||
|
style: { backgroundColor: cat.color },
|
||||||
|
}),
|
||||||
|
`${cat.icon} ${cat.label}`,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return el('div', { className: 'filter-panel__categories' }, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSpecialRequirementsGroup() {
|
||||||
|
refs.specialRequirementInputs = SPECIAL_REQUIREMENTS.map((label, index) => {
|
||||||
|
const id = `pg-requirement-${index}`;
|
||||||
|
const input = el('input', {
|
||||||
|
attrs: { type: 'checkbox', id, value: label },
|
||||||
|
on: { change: updateOutput },
|
||||||
|
});
|
||||||
|
return { input, label };
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = refs.specialRequirementInputs.map(({ input, label }) =>
|
||||||
|
el('label', { className: 'form-checkbox', attrs: { for: input.id } }, [input, label])
|
||||||
|
);
|
||||||
|
|
||||||
|
return el('div', { className: 'place-form__checkbox-group' }, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Die eigentliche id/for-Verdrahtung übernimmt field(); hier nur die
|
||||||
|
// <option>-Liste erzeugen.
|
||||||
|
function buildSelect(options) {
|
||||||
|
return el(
|
||||||
|
'select',
|
||||||
|
{ on: { change: updateOutput } },
|
||||||
|
options.map((opt) => el('option', { attrs: { value: opt.value }, text: opt.label }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSkeleton(rootContainer) {
|
||||||
|
refs = {};
|
||||||
|
|
||||||
|
refs.backdrop = el('div', {
|
||||||
|
className: 'prompt-generator-modal__backdrop',
|
||||||
|
on: { click: () => closePromptGenerator() },
|
||||||
|
});
|
||||||
|
|
||||||
|
refs.closeBtn = el(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
className: 'prompt-generator-modal__close',
|
||||||
|
attrs: { type: 'button', 'aria-label': 'KI-Prompt-Generator schließen' },
|
||||||
|
on: { click: () => closePromptGenerator() },
|
||||||
|
},
|
||||||
|
['✕']
|
||||||
|
);
|
||||||
|
|
||||||
|
refs.title = el('h2', {
|
||||||
|
className: 'prompt-generator__title',
|
||||||
|
attrs: { id: 'prompt-generator-title' },
|
||||||
|
text: '🤖 Neue Ausflugsziele mit KI erstellen',
|
||||||
|
});
|
||||||
|
|
||||||
|
refs.intro = el('p', {
|
||||||
|
className: 'prompt-generator__intro',
|
||||||
|
text:
|
||||||
|
'Fülle das Formular aus, um automatisch einen fertigen Prompt für eine externe KI ' +
|
||||||
|
'(z.B. ChatGPT, Claude, Gemini) zu erzeugen. Der Prompt bittet die KI, passende ' +
|
||||||
|
'Ausflugsziele zu recherchieren und als strukturiertes JSON zurückzugeben.',
|
||||||
|
});
|
||||||
|
|
||||||
|
// -- Eingabefelder --
|
||||||
|
|
||||||
|
refs.region = el('input', {
|
||||||
|
attrs: { type: 'text', placeholder: 'z.B. München, Chiemgau, Bayern …' },
|
||||||
|
on: { input: updateOutput },
|
||||||
|
});
|
||||||
|
|
||||||
|
refs.locationHint = el('p', { className: 'filter-panel__hint' });
|
||||||
|
refs.locationHint.hidden = true;
|
||||||
|
refs.useLocationBtn = el(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
className: 'btn btn--secondary prompt-generator__location-btn',
|
||||||
|
attrs: { type: 'button' },
|
||||||
|
on: { click: handleUseLocationClick },
|
||||||
|
},
|
||||||
|
['Standort als Suchgebiet übernehmen']
|
||||||
|
);
|
||||||
|
refs.useLocationBtn.hidden = true;
|
||||||
|
|
||||||
|
refs.radius = el('input', {
|
||||||
|
attrs: { type: 'number', min: '1', step: '1', placeholder: 'z.B. 30' },
|
||||||
|
on: { input: updateOutput },
|
||||||
|
});
|
||||||
|
|
||||||
|
refs.ageMin = el('input', {
|
||||||
|
attrs: { type: 'number', min: '0', step: '1', placeholder: 'ab' },
|
||||||
|
on: { input: updateOutput },
|
||||||
|
});
|
||||||
|
refs.ageMax = el('input', {
|
||||||
|
attrs: { type: 'number', min: '0', step: '1', placeholder: 'bis' },
|
||||||
|
on: { input: updateOutput },
|
||||||
|
});
|
||||||
|
|
||||||
|
refs.environment = buildSelect(ENVIRONMENT_OPTIONS);
|
||||||
|
refs.costRange = buildSelect(COST_RANGE_OPTIONS);
|
||||||
|
refs.costDetails = el('input', {
|
||||||
|
attrs: { type: 'text', placeholder: 'z.B. maximal 15 € Eintritt pro Familie (optional)' },
|
||||||
|
on: { input: updateOutput },
|
||||||
|
});
|
||||||
|
|
||||||
|
refs.maxResults = el('input', {
|
||||||
|
attrs: { type: 'number', min: '1', step: '1', value: '10' },
|
||||||
|
on: { input: updateOutput },
|
||||||
|
});
|
||||||
|
|
||||||
|
refs.additionalRequirements = el('textarea', {
|
||||||
|
attrs: { placeholder: 'Weitere, oben nicht aufgeführte Anforderungen (optional)' },
|
||||||
|
on: { input: updateOutput },
|
||||||
|
});
|
||||||
|
|
||||||
|
refs.outputLanguage = buildSelect(LANGUAGE_OPTIONS);
|
||||||
|
|
||||||
|
const form = el('div', { className: 'place-form prompt-generator__form' }, [
|
||||||
|
section('Suchgebiet', [
|
||||||
|
field('pg-region', 'Region, Ort oder Suchgebiet', refs.region, { required: true }),
|
||||||
|
refs.locationHint,
|
||||||
|
refs.useLocationBtn,
|
||||||
|
field('pg-radius', 'Maximaler Radius (km)', refs.radius),
|
||||||
|
]),
|
||||||
|
section('Kategorien', [buildCategoryGroup()]),
|
||||||
|
section('Altersgruppe der Kinder', [
|
||||||
|
row([
|
||||||
|
field('pg-age-min', 'Alter ab (Jahre)', refs.ageMin),
|
||||||
|
field('pg-age-max', 'Alter bis (Jahre)', refs.ageMax),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
section('Umgebung & Kosten', [
|
||||||
|
field('pg-environment-field', 'Indoor, Outdoor oder beides', refs.environment),
|
||||||
|
field('pg-cost-range-field', 'Kostenrahmen', refs.costRange),
|
||||||
|
field('pg-cost-details-field', 'Kostenrahmen – zusätzliche Angabe', refs.costDetails),
|
||||||
|
]),
|
||||||
|
section('Ergebnisse', [field('pg-max-results-field', 'Maximale Anzahl an Ergebnissen', refs.maxResults)]),
|
||||||
|
section('Besondere Anforderungen', [
|
||||||
|
buildSpecialRequirementsGroup(),
|
||||||
|
field('pg-additional-requirements-field', 'Zusätzliche Anforderungen (Freitext)', refs.additionalRequirements),
|
||||||
|
]),
|
||||||
|
section('Sprache der Ausgabe', [
|
||||||
|
field('pg-output-language-field', 'Sprache der KI-Ergebnistexte', refs.outputLanguage),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// -- Ausgabebereich --
|
||||||
|
|
||||||
|
refs.output = el('textarea', {
|
||||||
|
className: 'prompt-generator__output-textarea',
|
||||||
|
attrs: { readonly: '', id: 'prompt-generator-output', rows: '16' },
|
||||||
|
});
|
||||||
|
|
||||||
|
refs.copyBtn = el(
|
||||||
|
'button',
|
||||||
|
{ className: 'btn btn--primary', attrs: { type: 'button' }, on: { click: handleCopyClick } },
|
||||||
|
['In Zwischenablage kopieren']
|
||||||
|
);
|
||||||
|
refs.downloadBtn = el(
|
||||||
|
'button',
|
||||||
|
{ className: 'btn btn--secondary', attrs: { type: 'button' }, on: { click: handleDownloadClick } },
|
||||||
|
['Als Textdatei speichern']
|
||||||
|
);
|
||||||
|
refs.copyStatus = el('p', { className: 'prompt-generator__status', attrs: { role: 'status' } });
|
||||||
|
|
||||||
|
const outputSection = el('div', { className: 'prompt-generator__output' }, [
|
||||||
|
el('h3', { className: 'place-form__section-label', text: 'Generierter Prompt' }),
|
||||||
|
refs.output,
|
||||||
|
el('div', { className: 'prompt-generator__output-actions' }, [refs.copyBtn, refs.downloadBtn, refs.copyStatus]),
|
||||||
|
el('div', { className: 'prompt-generator__instructions' }, [
|
||||||
|
el('h3', { className: 'place-form__section-label', text: 'Anleitung' }),
|
||||||
|
el('ol', { className: 'prompt-generator__instructions-list' }, [
|
||||||
|
el('li', { text: 'Formular ausfüllen.' }),
|
||||||
|
el('li', { text: 'Prompt kopieren oder als Textdatei speichern.' }),
|
||||||
|
el('li', {
|
||||||
|
text: 'In eine KI (z.B. ChatGPT, Claude, Gemini) einfügen und die Antwort abwarten.',
|
||||||
|
}),
|
||||||
|
el('li', {
|
||||||
|
text:
|
||||||
|
'Die JSON-Antwort der KI im nächsten Schritt (KI-JSON-Import, in einer späteren Version ' +
|
||||||
|
'dieser Anwendung) prüfen und importieren.',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const layout = el('div', { className: 'prompt-generator__layout' }, [form, outputSection]);
|
||||||
|
|
||||||
|
refs.panel = el(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
className: 'prompt-generator-modal__panel',
|
||||||
|
attrs: { role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'prompt-generator-title' },
|
||||||
|
},
|
||||||
|
[refs.closeBtn, refs.title, refs.intro, layout]
|
||||||
|
);
|
||||||
|
|
||||||
|
rootContainer.appendChild(refs.backdrop);
|
||||||
|
rootContainer.appendChild(refs.panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCheckedValues(inputs) {
|
||||||
|
return inputs.filter((input) => input.checked).map((input) => input.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseOptionalNumber(raw) {
|
||||||
|
const trimmed = (raw || '').trim();
|
||||||
|
if (trimmed === '') return null;
|
||||||
|
const num = Number(trimmed);
|
||||||
|
return Number.isFinite(num) ? num : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readFormValues() {
|
||||||
|
const { CATEGORIES } = EventMap.config;
|
||||||
|
const selectedCategoryIds = readCheckedValues(refs.categoryInputs);
|
||||||
|
const categoryLabels = CATEGORIES.filter((cat) => selectedCategoryIds.includes(cat.id)).map(
|
||||||
|
(cat) => cat.label
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
region: refs.region.value,
|
||||||
|
radiusKm: parseOptionalNumber(refs.radius.value),
|
||||||
|
categoryLabels,
|
||||||
|
ageMin: parseOptionalNumber(refs.ageMin.value),
|
||||||
|
ageMax: parseOptionalNumber(refs.ageMax.value),
|
||||||
|
environment: refs.environment.value,
|
||||||
|
costRange: refs.costRange.value,
|
||||||
|
costDetails: refs.costDetails.value,
|
||||||
|
maxResults: parseOptionalNumber(refs.maxResults.value) || 10,
|
||||||
|
specialRequirements: readCheckedValues(refs.specialRequirementInputs.map((r) => r.input)),
|
||||||
|
additionalRequirements: refs.additionalRequirements.value,
|
||||||
|
outputLanguage: refs.outputLanguage.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Berechnet den Prompt live neu und zeigt ihn im Ausgabefeld an. Ohne gültige Region wird stattdessen ein Hinweis angezeigt. */
|
||||||
|
function updateOutput() {
|
||||||
|
const values = readFormValues();
|
||||||
|
const hasRegion = (values.region || '').trim() !== '';
|
||||||
|
|
||||||
|
if (!hasRegion) {
|
||||||
|
refs.output.value = '(Bitte zunächst eine Region, einen Ort oder ein Suchgebiet angeben, um den Prompt zu erzeugen.)';
|
||||||
|
refs.copyBtn.disabled = true;
|
||||||
|
refs.downloadBtn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refs.output.value = EventMap.ai.buildPrompt(values);
|
||||||
|
refs.copyBtn.disabled = false;
|
||||||
|
refs.downloadBtn.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetCopyStatus() {
|
||||||
|
refs.copyStatus.textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCopyClick() {
|
||||||
|
const success = await EventMap.utils.copyToClipboard(refs.output.value);
|
||||||
|
const defaultLabel = 'In Zwischenablage kopieren';
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
refs.copyBtn.textContent = 'Kopiert!';
|
||||||
|
refs.copyStatus.textContent = '';
|
||||||
|
} else {
|
||||||
|
refs.copyBtn.textContent = defaultLabel;
|
||||||
|
refs.copyStatus.textContent =
|
||||||
|
'Kopieren war nicht möglich. Bitte den Text im Feld oben manuell markieren und kopieren.';
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
refs.copyBtn.textContent = defaultLabel;
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDownloadClick() {
|
||||||
|
EventMap.utils.downloadTextFile('eventmap-ki-prompt.txt', refs.output.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUseLocationClick() {
|
||||||
|
const { userLocation } = EventMap.store.getState();
|
||||||
|
if (!userLocation) return;
|
||||||
|
|
||||||
|
refs.region.value = `Umkreis um ${userLocation.lat.toFixed(4)}, ${userLocation.lng.toFixed(4)}`;
|
||||||
|
updateOutput();
|
||||||
|
refs.region.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncLocationHint() {
|
||||||
|
const { userLocation } = EventMap.store.getState();
|
||||||
|
const available = Boolean(userLocation);
|
||||||
|
refs.locationHint.hidden = !available;
|
||||||
|
refs.useLocationBtn.hidden = !available;
|
||||||
|
if (available) {
|
||||||
|
refs.locationHint.textContent = `Dein aktueller Standort ist bekannt (${userLocation.lat.toFixed(4)}, ${userLocation.lng.toFixed(4)}).`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Öffnet den KI-Prompt-Generator. */
|
||||||
|
function openPromptGenerator() {
|
||||||
|
if (!container || !refs) {
|
||||||
|
console.error('EventMap: Prompt-Generator wurde noch nicht initialisiert (initPromptGenerator()).');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncLocationHint();
|
||||||
|
resetCopyStatus();
|
||||||
|
updateOutput();
|
||||||
|
|
||||||
|
container.hidden = false;
|
||||||
|
document.body.classList.add('prompt-generator-modal-open');
|
||||||
|
refs.region.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePromptGenerator() {
|
||||||
|
if (!container || container.hidden) return;
|
||||||
|
container.hidden = true;
|
||||||
|
document.body.classList.remove('prompt-generator-modal-open');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Initialisiert das Prompt-Generator-Modal im Element mit der übergebenen id. */
|
||||||
|
function initPromptGenerator(containerId) {
|
||||||
|
const containerEl = document.getElementById(containerId);
|
||||||
|
if (!containerEl) {
|
||||||
|
console.error(`EventMap: Prompt-Generator-Container #${containerId} nicht gefunden.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container = containerEl;
|
||||||
|
clearChildren(container);
|
||||||
|
buildSkeleton(container);
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Escape' && !container.hidden) {
|
||||||
|
closePromptGenerator();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
EventMap.ui.initPromptGenerator = initPromptGenerator;
|
||||||
|
EventMap.ui.openPromptGenerator = openPromptGenerator;
|
||||||
|
})();
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// Zwischenablage-Utility für den KI-Prompt-Generator (Phase 6). Nutzt primär
|
||||||
|
// die moderne Clipboard API, fällt aber auf das ältere
|
||||||
|
// document.execCommand('copy') zurück, falls navigator.clipboard nicht
|
||||||
|
// verfügbar ist oder der Aufruf fehlschlägt (z.B. weil manche Browser die
|
||||||
|
// Clipboard API unter file:// einschränken).
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
/** Kopiert per unsichtbarem <textarea> + execCommand('copy'). Gibt boolean zurück. */
|
||||||
|
function copyViaExecCommand(text) {
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.value = text;
|
||||||
|
// Unsichtbar, aber im Layout-Fluss vorhanden, damit select() zuverlässig funktioniert.
|
||||||
|
textarea.style.position = 'fixed';
|
||||||
|
textarea.style.top = '-1000px';
|
||||||
|
textarea.style.left = '-1000px';
|
||||||
|
textarea.setAttribute('readonly', '');
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
textarea.setSelectionRange(0, textarea.value.length);
|
||||||
|
|
||||||
|
let success = false;
|
||||||
|
try {
|
||||||
|
success = document.execCommand('copy');
|
||||||
|
} catch (err) {
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kopiert einen Text in die Zwischenablage.
|
||||||
|
* @param {string} text
|
||||||
|
* @returns {Promise<boolean>} true, wenn das Kopieren erfolgreich war.
|
||||||
|
*/
|
||||||
|
async function copyToClipboard(text) {
|
||||||
|
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
// Fällt unten auf den execCommand-Fallback zurück (z.B. Berechtigung
|
||||||
|
// verweigert oder API unter file:// eingeschränkt).
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return copyViaExecCommand(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
EventMap.utils.copyToClipboard = copyToClipboard;
|
||||||
|
})();
|
||||||
@@ -16,6 +16,7 @@ window.EventMap.store = window.EventMap.store || {};
|
|||||||
window.EventMap.map = window.EventMap.map || {};
|
window.EventMap.map = window.EventMap.map || {};
|
||||||
window.EventMap.ui = window.EventMap.ui || {};
|
window.EventMap.ui = window.EventMap.ui || {};
|
||||||
window.EventMap.filters = window.EventMap.filters || {};
|
window.EventMap.filters = window.EventMap.filters || {};
|
||||||
|
window.EventMap.ai = window.EventMap.ai || {};
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// Download-Utility für den KI-Prompt-Generator (Phase 6): erzeugt clientseitig
|
||||||
|
// einen Blob und triggert den Download über ein unsichtbares
|
||||||
|
// <a download>-Element. Funktioniert rein clientseitig, also auch unter
|
||||||
|
// file:// ohne lokalen Server.
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
/**
|
||||||
|
* Speichert Text als Datei über den Browser-Download.
|
||||||
|
* @param {string} filename
|
||||||
|
* @param {string} content
|
||||||
|
*/
|
||||||
|
function downloadTextFile(filename, content) {
|
||||||
|
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
link.style.display = 'none';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
|
||||||
|
// URL erst leicht verzögert freigeben, damit der Download-Vorgang
|
||||||
|
// in jedem Browser sicher gestartet werden konnte.
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
EventMap.utils.downloadTextFile = downloadTextFile;
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user