'use strict'; /** * Einstellungsdialog. Liest und schreibt dieselbe config.json, die auch von * Hand bearbeitet werden kann — unbekannte Felder und die `_`-Kommentare * bleiben dabei erhalten. */ const $ = (id) => document.getElementById(id); const el = { path: $('config-path'), close: $('btn-close'), save: $('btn-save'), revert: $('btn-revert'), openFile: $('btn-open-file'), feedback: $('feedback'), opacity: $('opacity'), opacityOut: $('opacity-out'), clickThrough: $('clickThrough'), autostart: $('autostart'), autostartLabel: $('autostart-label'), oauthEnabled: $('oauthEnabled'), oauthEndpoint: $('oauthEndpoint'), limitWeek: $('limitWeek'), limitBlock: $('limitBlock'), wInput: $('wInput'), wOutput: $('wOutput'), wCacheCreate: $('wCacheCreate'), wCacheRead: $('wCacheRead'), mOpus: $('mOpus'), mSonnet: $('mSonnet'), mHaiku: $('mHaiku'), weekday: $('weekday'), resetHour: $('resetHour'), resetMinute: $('resetMinute'), }; /** Voreinstellungen, die auch der Main-Prozess verwendet. */ const DEFAULTS = { opacity: 1, tokenWeights: { input: 1, output: 5, cacheCreate: 1.25, cacheRead: 0.1 }, modelWeights: { opus: 5, sonnet: 1, haiku: 0.3 }, }; let feedbackTimer = null; function say(text, kind = 'ok') { el.feedback.hidden = false; el.feedback.textContent = text; el.feedback.dataset.kind = kind; clearTimeout(feedbackTimer); feedbackTimer = setTimeout(() => { el.feedback.hidden = true; }, 4000); } /** Zahl aus einem Feld; leer ergibt null. */ function num(input) { const raw = input.value.trim(); if (raw === '') return null; const value = Number(raw.replace(',', '.')); return Number.isFinite(value) ? value : NaN; } function setNum(input, value, fallback = '') { input.value = value == null ? fallback : String(value); } /** Füllt die Oberfläche aus der Konfiguration. */ function fill(config, meta) { el.path.textContent = meta.configPath; const opacity = Math.round((config.opacity ?? DEFAULTS.opacity) * 100); el.opacity.value = String(opacity); el.opacityOut.textContent = `${opacity} %`; el.clickThrough.checked = Boolean(config.clickThrough); el.autostart.checked = Boolean(meta.autostart); el.autostartLabel.textContent = meta.platform === 'win32' ? 'Mit Windows starten' : 'Mit dem System starten'; const oauth = config.oauth || {}; el.oauthEnabled.checked = oauth.enabled !== false; el.oauthEndpoint.value = oauth.endpoint || ''; const limits = config.limits || {}; setNum(el.limitWeek, limits.week); setNum(el.limitBlock, limits.block); const tw = { ...DEFAULTS.tokenWeights, ...(config.tokenWeights || {}) }; setNum(el.wInput, tw.input); setNum(el.wOutput, tw.output); setNum(el.wCacheCreate, tw.cacheCreate); setNum(el.wCacheRead, tw.cacheRead); const mw = { ...DEFAULTS.modelWeights, ...(config.modelWeights || {}) }; setNum(el.mOpus, mw.opus); setNum(el.mSonnet, mw.sonnet); setNum(el.mHaiku, mw.haiku); el.weekday.value = config.weekResetWeekday == null ? '' : String(config.weekResetWeekday); setNum(el.resetHour, config.weekResetHour, '0'); setNum(el.resetMinute, config.weekResetMinute, '0'); updateWeekFields(); } /** Reset-Zeit ist ohne gewählten Wochentag bedeutungslos. */ function updateWeekFields() { const off = el.weekday.value === ''; el.resetHour.disabled = off; el.resetMinute.disabled = off; } /** * Baut das Änderungsobjekt. Nur gesetzte Werte werden übernommen — so bleiben * Felder, die der Dialog nicht kennt, in der Datei unangetastet. * @returns {{patch: Object, errors: string[]}} */ function collect() { const errors = []; const patch = {}; const check = (input, label, { min = 0, max = Infinity, required = false } = {}) => { const value = num(input); const invalid = Number.isNaN(value) || (value != null && (value < min || value > max)) || (required && value == null); input.setAttribute('aria-invalid', invalid ? 'true' : 'false'); if (invalid) errors.push(label); return invalid ? undefined : value; }; patch.opacity = Number(el.opacity.value) / 100; patch.clickThrough = el.clickThrough.checked; patch.oauth = { enabled: el.oauthEnabled.checked }; const endpoint = el.oauthEndpoint.value.trim(); if (endpoint) { if (!/^https:\/\/[^\s]+$/.test(endpoint)) errors.push('Endpunkt'); else patch.oauth.endpoint = endpoint; } else { patch.oauth.endpoint = ''; } const week = check(el.limitWeek, 'Bezugsgröße Woche'); const block = check(el.limitBlock, 'Bezugsgröße 5-Stunden-Fenster'); patch.limits = {}; if (week) patch.limits.week = week; if (block) patch.limits.block = block; patch.tokenWeights = { input: check(el.wInput, 'Gewicht Eingabe', { required: true }), output: check(el.wOutput, 'Gewicht Ausgabe', { required: true }), cacheCreate: check(el.wCacheCreate, 'Gewicht Cache anlegen', { required: true }), cacheRead: check(el.wCacheRead, 'Gewicht Cache lesen', { required: true }), }; patch.modelWeights = { opus: check(el.mOpus, 'Gewicht Opus', { required: true }), sonnet: check(el.mSonnet, 'Gewicht Sonnet', { required: true }), haiku: check(el.mHaiku, 'Gewicht Haiku', { required: true }), }; if (el.weekday.value === '') { patch.weekResetWeekday = null; patch.weekResetHour = null; patch.weekResetMinute = null; } else { patch.weekResetWeekday = Number(el.weekday.value); patch.weekResetHour = check(el.resetHour, 'Reset-Stunde', { max: 23, required: true }); patch.weekResetMinute = check(el.resetMinute, 'Reset-Minute', { max: 59, required: true }); } return { patch, errors }; } async function load() { const { config, meta } = await window.settings.load(); fill(config, meta); } el.opacity.addEventListener('input', () => { el.opacityOut.textContent = `${el.opacity.value} %`; }); el.weekday.addEventListener('change', updateWeekFields); // Autostart ist keine Einstellung der Datei, sondern ein Systemzustand — // deshalb sofort wirksam statt beim Speichern. el.autostart.addEventListener('change', async () => { await window.settings.setAutostart(el.autostart.checked); say(el.autostart.checked ? 'Autostart aktiviert.' : 'Autostart deaktiviert.'); }); el.save.addEventListener('click', async () => { const { patch, errors } = collect(); if (errors.length) { say(`Bitte prüfen: ${errors.join(', ')}`, 'error'); return; } el.save.disabled = true; const result = await window.settings.save(patch); el.save.disabled = false; say(result.ok ? 'Gespeichert und übernommen.' : `Fehler: ${result.error}`, result.ok ? 'ok' : 'error'); }); el.revert.addEventListener('click', async () => { await load(); say('Änderungen verworfen.'); }); el.openFile.addEventListener('click', () => window.settings.openFile()); el.close.addEventListener('click', () => window.settings.close()); document.addEventListener('keydown', (event) => { if (event.key === 'Escape') window.settings.close(); if (event.key === 's' && (event.ctrlKey || event.metaKey)) { event.preventDefault(); el.save.click(); } }); load();