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:
2026-07-24 12:32:37 +02:00
parent dac952fbf9
commit 5e2197ec24
9 changed files with 963 additions and 2 deletions
+237
View File
@@ -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
View File
@@ -3,7 +3,7 @@
(function () {
const { initStore } = EventMap.store;
const { initMap, centerOnCoords, requestUserLocation } = EventMap.map;
const { initList, initDetail, initForm, initFilterPanel } = EventMap.ui;
const { initList, initDetail, initForm, initFilterPanel, initPromptGenerator } = EventMap.ui;
function showFatalError(message) {
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() {
await initStore();
initMap('map');
@@ -55,8 +64,10 @@
initList('place-list');
initDetail('detail-modal');
initForm('place-form-modal');
initPromptGenerator('prompt-generator-modal');
setupGeolocationButton();
setupAddPlaceButton();
setupPromptGeneratorButton();
}
bootstrap().catch((err) => {
+442
View File
@@ -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;
})();
+52
View File
@@ -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;
})();
+1
View File
@@ -16,6 +16,7 @@ window.EventMap.store = window.EventMap.store || {};
window.EventMap.map = window.EventMap.map || {};
window.EventMap.ui = window.EventMap.ui || {};
window.EventMap.filters = window.EventMap.filters || {};
window.EventMap.ai = window.EventMap.ai || {};
(function () {
/**
+30
View File
@@ -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;
})();