import { Component } from 'react'; import Alert from '@mui/material/Alert'; import Box from '@mui/material/Box'; import CircularProgress from '@mui/material/CircularProgress'; import IconButton from '@mui/material/IconButton'; import MenuItem from '@mui/material/MenuItem'; import Stack from '@mui/material/Stack'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; import TableCell from '@mui/material/TableCell'; import TableContainer from '@mui/material/TableContainer'; import TableRow from '@mui/material/TableRow'; import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined'; import { enqueueSnackbar } from 'notistack'; import BackendError from '../BackendError'; import { setting as settingApi } from '../../api'; import companyAutomat from '../../automats/companyAutomat'; import { requireArray, requireDefined, requireObject } from '../../utils/apiShape'; import { backendErrorFrom } from '../../utils/backendError'; import { notifyError } from '../../utils/notify'; /** * Settings that may be set via PUT /api/company/setting/{key}. * Only these keys are accepted by the backend. */ const SETTABLE_KEYS = [ 'ui.theme', 'keys.tavily', 'keys.openrouter', 'keys.serper', 'modelslugimage', 'systemprompt-prefix', 'systemprompt-suffix', ]; const KEY_LABELS = { 'ui.theme': 'UI-Theme', 'keys.tavily': 'Tavily API-Key', 'keys.openrouter': 'OpenRouter API-Key', 'keys.serper': 'Serper API-Key', 'modelslugimage': 'Modellslug Bild', 'systemprompt-prefix': 'Prä Prompt', 'systemprompt-suffix': 'Post Prompt', }; export const DEFAULT_SETTINGS = { modelslugimage: 'meta/muse-image', }; const ENUM_OPTIONS = {}; const MULTILINE_KEYS = new Set([ 'systemprompt-prefix', 'systemprompt-suffix', ]); const EXCLUDED_KEYS = new Set([ 'labelprinter.addr', 'labelprinter.type', ]); function keyLabel(key) { return KEY_LABELS[key] ?? key; } export default class SettingsKeysPanel extends Component { constructor(props) { super(props); this.state = { isAdmin: companyAutomat.state.current?.role === 'admin', status: 'loading', error: null, settings: [], // key → { value, busy, saveError } drafts: {}, }; this.handleRetry = this.handleRetry.bind(this); this.handleSave = this.handleSave.bind(this); } componentDidMount() { this.unsub = companyAutomat.subscribe(this, (state) => ({ isAdmin: state.current?.role === 'admin', })); void this.load(); } async load() { this.setState({ status: 'loading', error: null }); try { const data = await settingApi.list(); requireObject(data, 'GET /api/company/setting'); const fetched = requireArray(data.settings, 'settings'); const byKey = new Map(fetched.map((s) => [s.key, s])); const settings = SETTABLE_KEYS.map((key) => { if (byKey.has(key)) return byKey.get(key); return { key, value: DEFAULT_SETTINGS[key] ?? null }; }); const drafts = Object.fromEntries( settings.map((s) => [ s.key, { value: s.value ?? (DEFAULT_SETTINGS[s.key] ?? ''), busy: false, saveError: null, }, ]), ); this.setState({ status: 'ready', settings, drafts }); } catch (err) { notifyError(err); this.setState({ status: 'failed', error: backendErrorFrom(err) }); } } handleRetry() { void this.load(); } handleChange(key, field, value) { this.setState((prev) => ({ drafts: { ...prev.drafts, [key]: { ...prev.drafts[key], [field]: value, saveError: null }, }, })); } async handleSave(key) { if (!this.state.isAdmin) return; const draft = this.state.drafts[key]; if (!draft || draft.busy) return; const row = this.state.settings.find((s) => s.key === key); if (!row) return; const valToSave = draft.value !== '' ? draft.value : (DEFAULT_SETTINGS[key] ?? ''); const body = { value: valToSave }; this.setState((prev) => ({ drafts: { ...prev.drafts, [key]: { ...prev.drafts[key], busy: true, saveError: null }, }, })); try { const data = await settingApi.upsert(key, body); requireObject(data, 'PUT /api/company/setting/:key'); requireObject(data.setting, 'setting'); const updated = requireDefined(data.setting, 'setting'); const settings = this.state.settings.map((s) => s.key === key ? { ...s, ...body } : s, ); this.setState({ settings, drafts: { ...this.state.drafts, [key]: { value: updated.value ?? valToSave, busy: false, saveError: null, }, }, }); enqueueSnackbar('Einstellung gespeichert.', { variant: 'success' }); } catch (err) { if (this._unmounted) return; this.setState((prev) => ({ drafts: { ...prev.drafts, [key]: { ...prev.drafts[key], busy: false, saveError: backendErrorFrom(err) }, }, })); } } componentWillUnmount() { this._unmounted = true; this.unsub?.(); } render() { const { isAdmin, status, error, settings, drafts } = this.state; if (status === 'loading') { return ( ); } if (status === 'failed') { return ; } if (settings.length === 0) { return ( Keine Einstellungen. ); } return ( {SETTABLE_KEYS.filter((k) => !EXCLUDED_KEYS.has(k)).map((key) => { const row = settings.find((s) => s.key === key); const draft = row ? drafts[row.key] : null; if (!row || !draft) return null; const isMultiline = MULTILINE_KEYS.has(row.key); const enumOpts = ENUM_OPTIONS[row.key]; return ( {keyLabel(row.key)} {enumOpts ? ( this.handleChange(row.key, 'value', e.target.value)} slotProps={{ htmlInput: { spellCheck: false } }} > {enumOpts.map((opt) => { const val = typeof opt === 'object' ? opt.value : opt; const lbl = typeof opt === 'object' ? opt.label : opt; return ( {lbl} ); })} ) : ( this.handleChange(row.key, 'value', e.target.value)} onKeyDown={(e) => { if (!isMultiline && e.key === 'Enter') { e.preventDefault(); void this.handleSave(row.key); } }} autoComplete="off" slotProps={{ htmlInput: { spellCheck: false, autoCorrect: 'off', autoCapitalize: 'off' } }} /> )} {draft.saveError ? ( {typeof draft.saveError === 'string' ? draft.saveError : draft.saveError.message} ) : null} void this.handleSave(row.key)} > ); })}
); } }