u
This commit is contained in:
@@ -60,7 +60,7 @@ import authAutomat from './authAutomat';
|
|||||||
|
|
||||||
/** Selectable model presets; the first entry is the default for new conversations. */
|
/** Selectable model presets; the first entry is the default for new conversations. */
|
||||||
export const PRESET_MODELS = [
|
export const PRESET_MODELS = [
|
||||||
{ value: 'stealth/space-bunny-alpha', label: 'Space Bunny Alpha' },
|
{ value: 'openai/gpt-6-luna', label: 'GPT-6 Luna (Standard)' },
|
||||||
];
|
];
|
||||||
const DEFAULT_MODEL = PRESET_MODELS[0].value;
|
const DEFAULT_MODEL = PRESET_MODELS[0].value;
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Box from '@mui/material/Box';
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
|
import InputBase from '@mui/material/InputBase';
|
||||||
import TextInput from '../TextInput';
|
import TextInput from '../TextInput';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import Select from '@mui/material/Select';
|
import Select from '@mui/material/Select';
|
||||||
@@ -18,8 +19,15 @@ import DeleteOutlineRoundedIcon from '@mui/icons-material/DeleteOutlineRounded';
|
|||||||
|
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import Badge from '@mui/material/Badge';
|
import Badge from '@mui/material/Badge';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
import AttachFileRoundedIcon from '@mui/icons-material/AttachFileRounded';
|
import AttachFileRoundedIcon from '@mui/icons-material/AttachFileRounded';
|
||||||
|
import TuneRoundedIcon from '@mui/icons-material/TuneRounded';
|
||||||
|
import ElderlyRoundedIcon from '@mui/icons-material/ElderlyRounded';
|
||||||
|
import DirectionsWalkRoundedIcon from '@mui/icons-material/DirectionsWalkRounded';
|
||||||
|
import DirectionsRunRoundedIcon from '@mui/icons-material/DirectionsRunRounded';
|
||||||
|
import PsychologyRoundedIcon from '@mui/icons-material/PsychologyRounded';
|
||||||
|
|
||||||
|
import { setting as settingApi } from '../../api';
|
||||||
import chatAutomat, { selectPendingAskUser } from '../../automats/chatAutomat';
|
import chatAutomat, { selectPendingAskUser } from '../../automats/chatAutomat';
|
||||||
import userSettingsAutomat from '../../automats/userSettingsAutomat';
|
import userSettingsAutomat from '../../automats/userSettingsAutomat';
|
||||||
import ChatFlowList from './ChatFlowList';
|
import ChatFlowList from './ChatFlowList';
|
||||||
@@ -62,6 +70,10 @@ export default class ChatDrawer extends PureComponent {
|
|||||||
attachmentError: null,
|
attachmentError: null,
|
||||||
drawerWidth: settings?.chatDrawerWidth || DEFAULT_DRAWER_WIDTH,
|
drawerWidth: settings?.chatDrawerWidth || DEFAULT_DRAWER_WIDTH,
|
||||||
isResizing: false,
|
isResizing: false,
|
||||||
|
showSettingsBar: false,
|
||||||
|
modelslugflag: 'floor',
|
||||||
|
modelreasoning: 'medium',
|
||||||
|
modelslug: 'openai/gpt-6-luna',
|
||||||
};
|
};
|
||||||
this.inputRef = React.createRef();
|
this.inputRef = React.createRef();
|
||||||
this.fileInputRef = React.createRef();
|
this.fileInputRef = React.createRef();
|
||||||
@@ -79,6 +91,7 @@ export default class ChatDrawer extends PureComponent {
|
|||||||
if (this.state.isOpen) {
|
if (this.state.isOpen) {
|
||||||
this.focusInput(150);
|
this.focusInput(150);
|
||||||
}
|
}
|
||||||
|
void this.loadModelSettings();
|
||||||
this.unsub = chatAutomat.subscribe(this, (state) => {
|
this.unsub = chatAutomat.subscribe(this, (state) => {
|
||||||
const wasOpen = this.state.isOpen;
|
const wasOpen = this.state.isOpen;
|
||||||
const willOpen = state.isOpen;
|
const willOpen = state.isOpen;
|
||||||
@@ -338,6 +351,71 @@ export default class ChatDrawer extends PureComponent {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
loadModelSettings = async () => {
|
||||||
|
try {
|
||||||
|
const data = await settingApi.list();
|
||||||
|
const settings = data?.settings || [];
|
||||||
|
const flagSetting = settings.find((s) => s.key === 'modelslugflag');
|
||||||
|
const reasoningSetting = settings.find((s) => s.key === 'modelreasoning');
|
||||||
|
const slugSetting = settings.find((s) => s.key === 'modelslug');
|
||||||
|
this.setState({
|
||||||
|
modelslugflag: flagSetting?.value ?? 'floor',
|
||||||
|
modelreasoning: reasoningSetting?.value ?? 'medium',
|
||||||
|
modelslug: slugSetting?.value ?? 'openai/gpt-6-luna',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[ChatDrawer] Failed to load model settings:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleToggleSettingsBar = () => {
|
||||||
|
this.setState((prev) => ({ showSettingsBar: !prev.showSettingsBar }));
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSetFlag = async (nextFlag) => {
|
||||||
|
if (this.state.modelslugflag === nextFlag) return;
|
||||||
|
const current = this.state.modelslugflag;
|
||||||
|
this.setState({ modelslugflag: nextFlag });
|
||||||
|
try {
|
||||||
|
await settingApi.upsert('modelslugflag', { value: nextFlag });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[ChatDrawer] Failed to save modelslugflag:', err);
|
||||||
|
this.setState({ modelslugflag: current });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSetReasoning = async (nextReasoning) => {
|
||||||
|
if (this.state.modelreasoning === nextReasoning) return;
|
||||||
|
const current = this.state.modelreasoning;
|
||||||
|
this.setState({ modelreasoning: nextReasoning });
|
||||||
|
try {
|
||||||
|
await settingApi.upsert('modelreasoning', { value: nextReasoning });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[ChatDrawer] Failed to save modelreasoning:', err);
|
||||||
|
this.setState({ modelreasoning: current });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleModelslugChange = (e) => {
|
||||||
|
this.setState({ modelslug: e.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleModelslugBlur = async () => {
|
||||||
|
const { modelslug } = this.state;
|
||||||
|
const trimmed = (modelslug || '').trim();
|
||||||
|
try {
|
||||||
|
await settingApi.upsert('modelslug', { value: trimmed });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[ChatDrawer] Failed to save modelslug:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleModelslugKeyDown = (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.target.blur();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
getPendingAskUser = () => selectPendingAskUser(this.state);
|
getPendingAskUser = () => selectPendingAskUser(this.state);
|
||||||
|
|
||||||
renderHeader() {
|
renderHeader() {
|
||||||
@@ -345,9 +423,12 @@ export default class ChatDrawer extends PureComponent {
|
|||||||
conversations,
|
conversations,
|
||||||
selectedConversationId,
|
selectedConversationId,
|
||||||
loadingConversations,
|
loadingConversations,
|
||||||
|
showSettingsBar,
|
||||||
|
modelslugflag,
|
||||||
|
modelreasoning,
|
||||||
|
modelslug,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -360,15 +441,261 @@ export default class ChatDrawer extends PureComponent {
|
|||||||
gap: 1.5,
|
gap: 1.5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', minHeight: 34 }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
{showSettingsBar ? (
|
||||||
<AutoAwesomeRoundedIcon sx={{ fontSize: 22, color: 'primary.main' }} />
|
/* Settings Bar Mode: Title goes away and reveals toggles */
|
||||||
<Typography variant="subtitle1" fontWeight={700}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, flex: 1, minWidth: 0, overflow: 'hidden' }}>
|
||||||
KI-Assistent
|
<IconButton
|
||||||
</Typography>
|
size="small"
|
||||||
</Box>
|
onClick={this.handleToggleSettingsBar}
|
||||||
|
aria-label="Einstellungen schließen"
|
||||||
|
sx={{
|
||||||
|
p: 0.5,
|
||||||
|
color: 'primary.main',
|
||||||
|
bgcolor: 'action.selected',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TuneRoundedIcon sx={{ fontSize: 18 }} />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
<IconButton size="small" onClick={this.handleClose} aria-label="Chat schließen">
|
{/* Group 1: Speed (floor, walk/standard "", run/nitro) */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.3,
|
||||||
|
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.05)'),
|
||||||
|
p: 0.3,
|
||||||
|
borderRadius: 2,
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => this.handleSetFlag('floor')}
|
||||||
|
aria-label="Floor"
|
||||||
|
sx={{
|
||||||
|
p: 0.3,
|
||||||
|
width: 26,
|
||||||
|
height: 26,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
bgcolor: modelslugflag === 'floor' ? 'primary.main' : 'transparent',
|
||||||
|
color: modelslugflag === 'floor' ? 'primary.contrastText' : 'text.secondary',
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: modelslugflag === 'floor' ? 'primary.dark' : 'action.selected',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ElderlyRoundedIcon sx={{ fontSize: 17 }} />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => this.handleSetFlag('')}
|
||||||
|
aria-label="Normal"
|
||||||
|
sx={{
|
||||||
|
p: 0.3,
|
||||||
|
width: 26,
|
||||||
|
height: 26,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
bgcolor: modelslugflag === '' ? 'primary.main' : 'transparent',
|
||||||
|
color: modelslugflag === '' ? 'primary.contrastText' : 'text.secondary',
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: modelslugflag === '' ? 'primary.dark' : 'action.selected',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DirectionsWalkRoundedIcon sx={{ fontSize: 17 }} />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => this.handleSetFlag('nitro')}
|
||||||
|
aria-label="Nitro"
|
||||||
|
sx={{
|
||||||
|
p: 0.3,
|
||||||
|
width: 26,
|
||||||
|
height: 26,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
bgcolor: modelslugflag === 'nitro' ? 'primary.main' : 'transparent',
|
||||||
|
color: modelslugflag === 'nitro' ? 'primary.contrastText' : 'text.secondary',
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: modelslugflag === 'nitro' ? 'primary.dark' : 'action.selected',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DirectionsRunRoundedIcon sx={{ fontSize: 17 }} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider orientation="vertical" flexItem sx={{ mx: 0.3, my: 0.5 }} />
|
||||||
|
|
||||||
|
{/* Group 2: Reasoning (none, low, medium, max) */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.3,
|
||||||
|
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.05)'),
|
||||||
|
p: 0.3,
|
||||||
|
borderRadius: 2,
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* none toggle (crossed out brain) */}
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => this.handleSetReasoning('none')}
|
||||||
|
aria-label="Reasoning None"
|
||||||
|
sx={{
|
||||||
|
p: 0.3,
|
||||||
|
width: 26,
|
||||||
|
height: 26,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
bgcolor: modelreasoning === 'none' ? 'primary.main' : 'transparent',
|
||||||
|
color: modelreasoning === 'none' ? 'primary.contrastText' : 'text.secondary',
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: modelreasoning === 'none' ? 'primary.dark' : 'action.selected',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ position: 'relative', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<PsychologyRoundedIcon sx={{ fontSize: 16 }} />
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
width: '125%',
|
||||||
|
height: '1.5px',
|
||||||
|
bgcolor: modelreasoning === 'none' ? 'primary.contrastText' : 'error.main',
|
||||||
|
transform: 'rotate(-45deg)',
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
{/* low toggle (smallest brain) */}
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => this.handleSetReasoning('low')}
|
||||||
|
aria-label="Reasoning Low"
|
||||||
|
sx={{
|
||||||
|
p: 0.3,
|
||||||
|
width: 26,
|
||||||
|
height: 26,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
bgcolor: modelreasoning === 'low' ? 'primary.main' : 'transparent',
|
||||||
|
color: modelreasoning === 'low' ? 'primary.contrastText' : 'text.secondary',
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: modelreasoning === 'low' ? 'primary.dark' : 'action.selected',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PsychologyRoundedIcon sx={{ fontSize: 13 }} />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
{/* medium toggle (medium brain) */}
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => this.handleSetReasoning('medium')}
|
||||||
|
aria-label="Reasoning Medium"
|
||||||
|
sx={{
|
||||||
|
p: 0.3,
|
||||||
|
width: 26,
|
||||||
|
height: 26,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
bgcolor: modelreasoning === 'medium' ? 'primary.main' : 'transparent',
|
||||||
|
color: modelreasoning === 'medium' ? 'primary.contrastText' : 'text.secondary',
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: modelreasoning === 'medium' ? 'primary.dark' : 'action.selected',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PsychologyRoundedIcon sx={{ fontSize: 17 }} />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
{/* max toggle (big brain) */}
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => this.handleSetReasoning('high')}
|
||||||
|
aria-label="Reasoning Max"
|
||||||
|
sx={{
|
||||||
|
p: 0.3,
|
||||||
|
width: 26,
|
||||||
|
height: 26,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
bgcolor: modelreasoning === 'high' ? 'primary.main' : 'transparent',
|
||||||
|
color: modelreasoning === 'high' ? 'primary.contrastText' : 'text.secondary',
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: modelreasoning === 'high' ? 'primary.dark' : 'action.selected',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PsychologyRoundedIcon sx={{ fontSize: 22 }} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider orientation="vertical" flexItem sx={{ mx: 0.3, my: 0.5 }} />
|
||||||
|
|
||||||
|
{/* modelslug input */}
|
||||||
|
<InputBase
|
||||||
|
value={modelslug || ''}
|
||||||
|
onChange={this.handleModelslugChange}
|
||||||
|
onBlur={this.handleModelslugBlur}
|
||||||
|
onKeyDown={this.handleModelslugKeyDown}
|
||||||
|
placeholder="Modellslug"
|
||||||
|
inputProps={{
|
||||||
|
spellCheck: false,
|
||||||
|
autoCorrect: 'off',
|
||||||
|
autoCapitalize: 'off',
|
||||||
|
'aria-label': 'Modellslug',
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 80,
|
||||||
|
fontSize: '0.78rem',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
px: 0.8,
|
||||||
|
py: 0.2,
|
||||||
|
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.05)'),
|
||||||
|
borderRadius: 1.5,
|
||||||
|
border: '1px solid transparent',
|
||||||
|
'&:focus-within': {
|
||||||
|
borderColor: 'primary.main',
|
||||||
|
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.1)' : '#fff'),
|
||||||
|
},
|
||||||
|
'& input': {
|
||||||
|
p: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
/* Normal Mode: settings icon on the left end, title visible */
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={this.handleToggleSettingsBar}
|
||||||
|
aria-label="Modell-Einstellungen"
|
||||||
|
sx={{
|
||||||
|
p: 0.5,
|
||||||
|
color: 'text.secondary',
|
||||||
|
'&:hover': { color: 'primary.main', bgcolor: 'action.hover' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TuneRoundedIcon sx={{ fontSize: 18 }} />
|
||||||
|
</IconButton>
|
||||||
|
<AutoAwesomeRoundedIcon sx={{ fontSize: 20, color: 'primary.main' }} />
|
||||||
|
<Typography variant="subtitle1" fontWeight={700}>
|
||||||
|
KI-Assistent
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<IconButton size="small" onClick={this.handleClose} aria-label="Chat schließen" sx={{ ml: 1, flexShrink: 0 }}>
|
||||||
<CloseRoundedIcon fontSize="small" />
|
<CloseRoundedIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Alert from '@mui/material/Alert';
|
|||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import Table from '@mui/material/Table';
|
import Table from '@mui/material/Table';
|
||||||
import TableBody from '@mui/material/TableBody';
|
import TableBody from '@mui/material/TableBody';
|
||||||
@@ -28,7 +29,7 @@ const SETTABLE_KEYS = [
|
|||||||
'keys.tavily',
|
'keys.tavily',
|
||||||
'keys.openrouter',
|
'keys.openrouter',
|
||||||
'keys.serper',
|
'keys.serper',
|
||||||
'default-model',
|
'modelslugimage',
|
||||||
'systemprompt-prefix',
|
'systemprompt-prefix',
|
||||||
'systemprompt-suffix',
|
'systemprompt-suffix',
|
||||||
];
|
];
|
||||||
@@ -38,11 +39,17 @@ const KEY_LABELS = {
|
|||||||
'keys.tavily': 'Tavily API-Key',
|
'keys.tavily': 'Tavily API-Key',
|
||||||
'keys.openrouter': 'OpenRouter API-Key',
|
'keys.openrouter': 'OpenRouter API-Key',
|
||||||
'keys.serper': 'Serper API-Key',
|
'keys.serper': 'Serper API-Key',
|
||||||
'default-model': 'Modellslug',
|
'modelslugimage': 'Modellslug Bild',
|
||||||
'systemprompt-prefix': 'Prä Prompt',
|
'systemprompt-prefix': 'Prä Prompt',
|
||||||
'systemprompt-suffix': 'Post Prompt',
|
'systemprompt-suffix': 'Post Prompt',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_SETTINGS = {
|
||||||
|
modelslugimage: 'meta/muse-image',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ENUM_OPTIONS = {};
|
||||||
|
|
||||||
const MULTILINE_KEYS = new Set([
|
const MULTILINE_KEYS = new Set([
|
||||||
'systemprompt-prefix',
|
'systemprompt-prefix',
|
||||||
'systemprompt-suffix',
|
'systemprompt-suffix',
|
||||||
@@ -80,12 +87,17 @@ export default class SettingsKeysPanel extends Component {
|
|||||||
try {
|
try {
|
||||||
const data = await settingApi.list();
|
const data = await settingApi.list();
|
||||||
requireObject(data, 'GET /api/company/setting');
|
requireObject(data, 'GET /api/company/setting');
|
||||||
const settings = requireArray(data.settings, 'settings');
|
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(
|
const drafts = Object.fromEntries(
|
||||||
settings.map((s) => [
|
settings.map((s) => [
|
||||||
s.key,
|
s.key,
|
||||||
{
|
{
|
||||||
value: s.value ?? '',
|
value: s.value ?? (DEFAULT_SETTINGS[s.key] ?? ''),
|
||||||
busy: false,
|
busy: false,
|
||||||
saveError: null,
|
saveError: null,
|
||||||
},
|
},
|
||||||
@@ -117,14 +129,8 @@ export default class SettingsKeysPanel extends Component {
|
|||||||
const row = this.state.settings.find((s) => s.key === key);
|
const row = this.state.settings.find((s) => s.key === key);
|
||||||
if (!row) return;
|
if (!row) return;
|
||||||
|
|
||||||
const body = {};
|
const valToSave = draft.value !== '' ? draft.value : (DEFAULT_SETTINGS[key] ?? '');
|
||||||
if (draft.value !== (row.value ?? '')) {
|
const body = { value: valToSave };
|
||||||
body.value = draft.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(body).length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState((prev) => ({
|
this.setState((prev) => ({
|
||||||
drafts: {
|
drafts: {
|
||||||
@@ -146,7 +152,7 @@ export default class SettingsKeysPanel extends Component {
|
|||||||
drafts: {
|
drafts: {
|
||||||
...this.state.drafts,
|
...this.state.drafts,
|
||||||
[key]: {
|
[key]: {
|
||||||
value: updated.value ?? '',
|
value: updated.value ?? valToSave,
|
||||||
busy: false,
|
busy: false,
|
||||||
saveError: null,
|
saveError: null,
|
||||||
},
|
},
|
||||||
@@ -198,6 +204,7 @@ export default class SettingsKeysPanel extends Component {
|
|||||||
const draft = row ? drafts[row.key] : null;
|
const draft = row ? drafts[row.key] : null;
|
||||||
if (!row || !draft) return null;
|
if (!row || !draft) return null;
|
||||||
const isMultiline = MULTILINE_KEYS.has(row.key);
|
const isMultiline = MULTILINE_KEYS.has(row.key);
|
||||||
|
const enumOpts = ENUM_OPTIONS[row.key];
|
||||||
return (
|
return (
|
||||||
<TableRow key={row.key} hover>
|
<TableRow key={row.key} hover>
|
||||||
<TableCell
|
<TableCell
|
||||||
@@ -211,23 +218,49 @@ export default class SettingsKeysPanel extends Component {
|
|||||||
<Typography variant="body2">{keyLabel(row.key)}</Typography>
|
<Typography variant="body2">{keyLabel(row.key)}</Typography>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell sx={{ verticalAlign: 'top' }}>
|
<TableCell sx={{ verticalAlign: 'top' }}>
|
||||||
<TextField
|
{enumOpts ? (
|
||||||
size="small"
|
<TextField
|
||||||
fullWidth
|
select
|
||||||
multiline={isMultiline}
|
size="small"
|
||||||
minRows={isMultiline ? 2 : undefined}
|
fullWidth
|
||||||
maxRows={isMultiline ? 6 : undefined}
|
value={draft.value !== undefined && draft.value !== null ? draft.value : (DEFAULT_SETTINGS[row.key] ?? '')}
|
||||||
value={draft.value}
|
disabled={draft.busy}
|
||||||
disabled={draft.busy}
|
onChange={(e) => this.handleChange(row.key, 'value', e.target.value)}
|
||||||
onChange={(e) => this.handleChange(row.key, 'value', e.target.value)}
|
slotProps={{ htmlInput: { spellCheck: false } }}
|
||||||
onKeyDown={(e) => {
|
inputProps={{ spellCheck: false }}
|
||||||
if (!isMultiline && e.key === 'Enter') {
|
>
|
||||||
e.preventDefault();
|
{enumOpts.map((opt) => {
|
||||||
void this.handleSave(row.key);
|
const val = typeof opt === 'object' ? opt.value : opt;
|
||||||
}
|
const lbl = typeof opt === 'object' ? opt.label : opt;
|
||||||
}}
|
return (
|
||||||
autoComplete="off"
|
<MenuItem key={val} value={val}>
|
||||||
/>
|
{lbl}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TextField>
|
||||||
|
) : (
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
multiline={isMultiline}
|
||||||
|
minRows={isMultiline ? 2 : undefined}
|
||||||
|
maxRows={isMultiline ? 6 : undefined}
|
||||||
|
placeholder={DEFAULT_SETTINGS[row.key] ?? undefined}
|
||||||
|
value={draft.value}
|
||||||
|
disabled={draft.busy}
|
||||||
|
onChange={(e) => 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' } }}
|
||||||
|
inputProps={{ spellCheck: false, autoCorrect: 'off', autoCapitalize: 'off' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right" sx={{ verticalAlign: 'top', pt: 1, width: 60 }}>
|
<TableCell align="right" sx={{ verticalAlign: 'top', pt: 1, width: 60 }}>
|
||||||
<Stack
|
<Stack
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import Box from '@mui/material/Box';
|
|||||||
import ButtonBase from '@mui/material/ButtonBase';
|
import ButtonBase from '@mui/material/ButtonBase';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { TILE_SIZE, Tile, tileBaseSx } from 'dashgui/tiles';
|
import { TILE_SIZE, Tile, tileBaseSx } from 'dashgui/tiles';
|
||||||
import { formatPrice } from '../../utils/price';
|
import { formatPrice, getArticleGross } from '../../utils/price';
|
||||||
|
import companyAutomat from '../../automats/companyAutomat';
|
||||||
import { articleSku } from '../article/articleRoute';
|
import { articleSku } from '../article/articleRoute';
|
||||||
import {
|
import {
|
||||||
getThumbHashDataUrl,
|
getThumbHashDataUrl,
|
||||||
@@ -289,6 +290,7 @@ export class ArticleTile extends Tile {
|
|||||||
if (!article) return null;
|
if (!article) return null;
|
||||||
const sku = articleSku(article);
|
const sku = articleSku(article);
|
||||||
const brand = article.brand;
|
const brand = article.brand;
|
||||||
|
const grossPrice = getArticleGross(article, companyAutomat.state.current?.taxrate);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{brand?.name ? (
|
{brand?.name ? (
|
||||||
@@ -310,9 +312,9 @@ export class ArticleTile extends Tile {
|
|||||||
{sku}
|
{sku}
|
||||||
</Typography>
|
</Typography>
|
||||||
) : null}
|
) : null}
|
||||||
{article.net != null && (
|
{grossPrice != null && (
|
||||||
<Typography variant="caption" fontWeight={600}>
|
<Typography variant="caption" fontWeight={600}>
|
||||||
{formatPrice(article.net)}
|
{formatPrice(grossPrice)}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -353,6 +355,7 @@ export class ArticleTile extends Tile {
|
|||||||
const hasPhoto = this.hasPhoto();
|
const hasPhoto = this.hasPhoto();
|
||||||
const { article, label, sx, disabled } = this.props;
|
const { article, label, sx, disabled } = this.props;
|
||||||
const sku = articleSku(article);
|
const sku = articleSku(article);
|
||||||
|
const grossPrice = getArticleGross(article, companyAutomat.state.current?.taxrate);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonBase
|
<ButtonBase
|
||||||
@@ -451,7 +454,7 @@ export class ArticleTile extends Tile {
|
|||||||
{sku}
|
{sku}
|
||||||
</Typography>
|
</Typography>
|
||||||
) : null}
|
) : null}
|
||||||
{article.net != null && (
|
{grossPrice != null && (
|
||||||
<Typography
|
<Typography
|
||||||
variant="caption"
|
variant="caption"
|
||||||
fontWeight={600}
|
fontWeight={600}
|
||||||
@@ -461,7 +464,7 @@ export class ArticleTile extends Tile {
|
|||||||
textShadow: TEXT_GLOW_DARK,
|
textShadow: TEXT_GLOW_DARK,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{formatPrice(article.net)}
|
{formatPrice(grossPrice)}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
|||||||
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
||||||
import articleSearchAutomat from '../automats/articleSearchAutomat';
|
import articleSearchAutomat from '../automats/articleSearchAutomat';
|
||||||
import BackendError from '../components/BackendError';
|
import BackendError from '../components/BackendError';
|
||||||
import { formatPrice } from '../utils/price';
|
import { formatPrice, getArticleGross } from '../utils/price';
|
||||||
import {
|
import {
|
||||||
buildSearchUrl,
|
buildSearchUrl,
|
||||||
queryFromParams,
|
queryFromParams,
|
||||||
@@ -46,6 +46,7 @@ import {
|
|||||||
setLastCategoryId,
|
setLastCategoryId,
|
||||||
navigationAutomat,
|
navigationAutomat,
|
||||||
categoryAutomat,
|
categoryAutomat,
|
||||||
|
companyAutomat,
|
||||||
} from '../automats';
|
} from '../automats';
|
||||||
import { BrandLabel } from '../components/brand';
|
import { BrandLabel } from '../components/brand';
|
||||||
import { SCRIM_LABEL } from '../colors';
|
import { SCRIM_LABEL } from '../colors';
|
||||||
@@ -586,7 +587,7 @@ class ArtikelSearchPage extends Component {
|
|||||||
const isParent = Boolean(row.isParent || row.is_parent);
|
const isParent = Boolean(row.isParent || row.is_parent);
|
||||||
const childCount = Number(row.childCount) || 0;
|
const childCount = Number(row.childCount) || 0;
|
||||||
const showChildBadge = isParent || childCount > 0;
|
const showChildBadge = isParent || childCount > 0;
|
||||||
const price = row.price ?? row.net ?? row.vknetto;
|
const price = getArticleGross(row, companyAutomat.state.current?.taxrate);
|
||||||
return (
|
return (
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
key={row.id}
|
key={row.id}
|
||||||
|
|||||||
@@ -52,3 +52,38 @@ export function netFromGross(gross, taxRate) {
|
|||||||
if (factor <= 0) return null;
|
if (factor <= 0) return null;
|
||||||
return Math.round((gross / factor) * 10000) / 10000;
|
return Math.round((gross / factor) * 10000) / 10000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves or calculates the gross price (Bruttopreis) for an article.
|
||||||
|
* Supports:
|
||||||
|
* - Direct gross price (`article.gross`)
|
||||||
|
* - Net price with tax rate (`article.net`, `article.taxRate`)
|
||||||
|
* - Nested prices array (`article.prices`)
|
||||||
|
*
|
||||||
|
* @param {object|null} article
|
||||||
|
* @param {number} [defaultTaxRate=0] Fallback tax rate if not present on article/price
|
||||||
|
* @returns {number|null} Gross price rounded to 2 decimal places, or null
|
||||||
|
*/
|
||||||
|
export function getArticleGross(article, defaultTaxRate = 0) {
|
||||||
|
if (!article) return null;
|
||||||
|
if (article.gross != null && Number.isFinite(Number(article.gross))) {
|
||||||
|
return Number(article.gross);
|
||||||
|
}
|
||||||
|
let net = article.net ?? article.price ?? article.vknetto;
|
||||||
|
let taxRate = article.taxRate;
|
||||||
|
if (net == null && Array.isArray(article.prices) && article.prices.length > 0) {
|
||||||
|
const base = article.prices.find((p) => p.minmultiplier === 1) ?? article.prices[0];
|
||||||
|
if (base?.gross != null && Number.isFinite(Number(base.gross))) {
|
||||||
|
return Number(base.gross);
|
||||||
|
}
|
||||||
|
net = base?.net;
|
||||||
|
if (taxRate == null) taxRate = base?.taxRate;
|
||||||
|
}
|
||||||
|
if (net == null || !Number.isFinite(Number(net))) return null;
|
||||||
|
const rate = taxRate != null && Number.isFinite(Number(taxRate))
|
||||||
|
? Number(taxRate)
|
||||||
|
: Number(defaultTaxRate ?? 0);
|
||||||
|
const gross = Number(net) * (1 + rate / 100);
|
||||||
|
return Math.round((gross + Number.EPSILON) * 100) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user