Seed-Daten per Fetch von externem Server laden, UI-Sprachumschalter (DE/EN) und KI-Übersetzungs-Prompt-Generator ergänzen
- Beispieldaten werden nicht mehr eingebettet (data/seed.js entfernt), sondern beim ersten Start per fetch(EventMap.config.SEED_DATA_URL) von einem externen, selbst gehosteten Server geladen (siehe ADR 0001, Revision 2026-07-28). - Neuer UI-Sprachumschalter (Deutsch/Englisch) für die Bedienoberfläche: js/i18n/strings.js + js/i18n/i18n.js, Sprachwahl im Menü, Persistenz über settings.language, Retranslation ohne Reload. - Neuer Menüpunkt "Ausflugsziel übersetzen (KI)": erzeugt einen Prompt, der eine externe KI bittet, Name/Beschreibungen eines bestehenden Ortes ins Englische, Spanische oder Französische zu übersetzen (js/ai/translatePromptBuilder.js, js/ui/translatePromptGenerator.js). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
// "Ausflugsziel übersetzen (KI)"-Modal: lässt Nutzer ein bestehendes
|
||||
// Ausflugsziel und eine Zielsprache (Englisch/Spanisch/Französisch)
|
||||
// auswählen und erzeugt daraus per EventMap.ai.buildTranslationPrompt()
|
||||
// (js/ai/translatePromptBuilder.js) einen fertigen, kopierbaren
|
||||
// Übersetzungs-Prompt für eine externe KI.
|
||||
//
|
||||
// Aufbau/Muster bewusst analog zu js/ui/promptGenerator.js (gleiche
|
||||
// Modal-Chrome-Klassen aus css/promptGenerator.css, gleicher Copy/Download-
|
||||
// Ablauf). Rendering ausschließlich über textContent/DOM-APIs (el()).
|
||||
//
|
||||
// WICHTIG: Diese Datei erzeugt nur den Prompt. Die JSON-Antwort der KI (mit
|
||||
// dem übersetzten "translations"-Feld) muss aktuell noch manuell in die
|
||||
// Ortsdaten übernommen werden - ein automatischer Import/Merge ist (noch)
|
||||
// nicht Teil dieser Datei.
|
||||
|
||||
(function () {
|
||||
const { el, clearChildren } = EventMap.utils;
|
||||
|
||||
const TARGET_LANGUAGE_OPTIONS = [
|
||||
{ value: 'en', label: 'Englisch' },
|
||||
{ value: 'es', label: 'Spanisch' },
|
||||
{ value: 'fr', label: 'Französisch' },
|
||||
];
|
||||
|
||||
let container = null;
|
||||
let refs = null;
|
||||
|
||||
function field(id, labelText, inputNode) {
|
||||
inputNode.id = id;
|
||||
return el('div', { className: 'form-field' }, [
|
||||
el('label', { className: 'form-field__label', attrs: { for: id } }, [labelText]),
|
||||
inputNode,
|
||||
]);
|
||||
}
|
||||
|
||||
function buildPlaceOptions() {
|
||||
const { places } = EventMap.store.getState();
|
||||
const sorted = [...places].sort((a, b) => (a.name || '').localeCompare(b.name || '', 'de'));
|
||||
|
||||
if (sorted.length === 0) {
|
||||
return [el('option', { attrs: { value: '' }, text: '(Keine Ausflugsziele vorhanden)' })];
|
||||
}
|
||||
|
||||
return sorted.map((place) =>
|
||||
el('option', {
|
||||
attrs: { value: place.id },
|
||||
text: place.city ? `${place.name} (${place.city})` : place.name,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function buildSkeleton(rootContainer) {
|
||||
refs = {};
|
||||
|
||||
refs.backdrop = el('div', {
|
||||
className: 'prompt-generator-modal__backdrop',
|
||||
on: { click: () => closeTranslatePromptGenerator() },
|
||||
});
|
||||
|
||||
refs.closeBtn = el(
|
||||
'button',
|
||||
{
|
||||
className: 'prompt-generator-modal__close',
|
||||
attrs: { type: 'button', 'aria-label': 'Übersetzungs-Prompt-Generator schließen' },
|
||||
on: { click: () => closeTranslatePromptGenerator() },
|
||||
},
|
||||
['✕']
|
||||
);
|
||||
|
||||
refs.title = el('h2', {
|
||||
className: 'prompt-generator__title',
|
||||
attrs: { id: 'translate-prompt-generator-title' },
|
||||
text: '🌐 Ausflugsziel übersetzen (KI)',
|
||||
});
|
||||
|
||||
refs.intro = el('p', {
|
||||
className: 'prompt-generator__intro',
|
||||
text:
|
||||
'Wähle ein bestehendes Ausflugsziel und eine Zielsprache aus, um einen fertigen Prompt für eine ' +
|
||||
'externe KI (z.B. ChatGPT, Claude, Gemini) zu erzeugen. Der Prompt bittet die KI, Name, Kurz- und ' +
|
||||
'ausführliche Beschreibung in die gewählte Sprache zu übersetzen.',
|
||||
});
|
||||
|
||||
refs.placeSelect = el('select', { on: { change: updateOutput } }, buildPlaceOptions());
|
||||
refs.languageSelect = el(
|
||||
'select',
|
||||
{ on: { change: updateOutput } },
|
||||
TARGET_LANGUAGE_OPTIONS.map((opt) => el('option', { attrs: { value: opt.value }, text: opt.label }))
|
||||
);
|
||||
|
||||
const form = el('div', { className: 'place-form prompt-generator__form' }, [
|
||||
el('fieldset', { className: 'place-form__section' }, [
|
||||
el('legend', { className: 'place-form__section-label', text: 'Auswahl' }),
|
||||
field('tpg-place', 'Ausflugsziel', refs.placeSelect),
|
||||
field('tpg-language', 'Zielsprache', refs.languageSelect),
|
||||
]),
|
||||
]);
|
||||
|
||||
refs.output = el('textarea', {
|
||||
className: 'prompt-generator__output-textarea',
|
||||
attrs: { readonly: '', id: 'translate-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: 'Ausflugsziel und Zielsprache auswählen.' }),
|
||||
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 enthält das Feld "translations" - dieses aktuell noch manuell in die ' +
|
||||
'gespeicherten Ortsdaten übernehmen (automatischer Import folgt ggf. später).',
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
|
||||
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': 'translate-prompt-generator-title' },
|
||||
},
|
||||
[refs.closeBtn, refs.title, refs.intro, layout]
|
||||
);
|
||||
|
||||
rootContainer.appendChild(refs.backdrop);
|
||||
rootContainer.appendChild(refs.panel);
|
||||
}
|
||||
|
||||
/** Berechnet den Prompt live neu. Ohne auswählbares Ausflugsziel wird stattdessen ein Hinweis angezeigt. */
|
||||
function updateOutput() {
|
||||
const { places } = EventMap.store.getState();
|
||||
const place = places.find((p) => p.id === refs.placeSelect.value);
|
||||
|
||||
if (!place) {
|
||||
refs.output.value = '(Bitte zuerst ein Ausflugsziel anlegen, um einen Übersetzungs-Prompt zu erzeugen.)';
|
||||
refs.copyBtn.disabled = true;
|
||||
refs.downloadBtn.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
refs.output.value = EventMap.ai.buildTranslationPrompt(place, refs.languageSelect.value);
|
||||
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-uebersetzung-prompt.txt', refs.output.value);
|
||||
}
|
||||
|
||||
/** Baut die Ortsauswahl neu auf (z.B. falls seit dem letzten Öffnen Orte hinzugekommen sind), behält die Auswahl falls noch gültig. */
|
||||
function refreshPlaceOptions() {
|
||||
const currentValue = refs.placeSelect.value;
|
||||
clearChildren(refs.placeSelect);
|
||||
buildPlaceOptions().forEach((option) => refs.placeSelect.appendChild(option));
|
||||
if ([...refs.placeSelect.options].some((opt) => opt.value === currentValue)) {
|
||||
refs.placeSelect.value = currentValue;
|
||||
}
|
||||
}
|
||||
|
||||
/** Öffnet den Übersetzungs-Prompt-Generator. */
|
||||
function openTranslatePromptGenerator() {
|
||||
if (!container || !refs) {
|
||||
console.error(
|
||||
'EventMap: Übersetzungs-Prompt-Generator wurde noch nicht initialisiert (initTranslatePromptGenerator()).'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
refreshPlaceOptions();
|
||||
resetCopyStatus();
|
||||
updateOutput();
|
||||
|
||||
container.hidden = false;
|
||||
document.body.classList.add('prompt-generator-modal-open');
|
||||
refs.placeSelect.focus();
|
||||
}
|
||||
|
||||
function closeTranslatePromptGenerator() {
|
||||
if (!container || container.hidden) return;
|
||||
container.hidden = true;
|
||||
document.body.classList.remove('prompt-generator-modal-open');
|
||||
}
|
||||
|
||||
/** Initialisiert das Übersetzungs-Prompt-Generator-Modal im Element mit der übergebenen id. */
|
||||
function initTranslatePromptGenerator(containerId) {
|
||||
const containerEl = document.getElementById(containerId);
|
||||
if (!containerEl) {
|
||||
console.error(`EventMap: Übersetzungs-Prompt-Generator-Container #${containerId} nicht gefunden.`);
|
||||
return;
|
||||
}
|
||||
|
||||
container = containerEl;
|
||||
clearChildren(container);
|
||||
buildSkeleton(container);
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape' && !container.hidden) {
|
||||
closeTranslatePromptGenerator();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
EventMap.ui.initTranslatePromptGenerator = initTranslatePromptGenerator;
|
||||
EventMap.ui.openTranslatePromptGenerator = openTranslatePromptGenerator;
|
||||
})();
|
||||
Reference in New Issue
Block a user