diff --git a/src/automats/chatAutomat.js b/src/automats/chatAutomat.js index bd6beee..3f28982 100644 --- a/src/automats/chatAutomat.js +++ b/src/automats/chatAutomat.js @@ -60,7 +60,7 @@ import authAutomat from './authAutomat'; /** Selectable model presets; the first entry is the default for new conversations. */ 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; diff --git a/src/components/chat/ChatDrawer.jsx b/src/components/chat/ChatDrawer.jsx index dec7408..5e06c4a 100644 --- a/src/components/chat/ChatDrawer.jsx +++ b/src/components/chat/ChatDrawer.jsx @@ -4,6 +4,7 @@ import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import IconButton from '@mui/material/IconButton'; import Button from '@mui/material/Button'; +import InputBase from '@mui/material/InputBase'; import TextInput from '../TextInput'; import MenuItem from '@mui/material/MenuItem'; import Select from '@mui/material/Select'; @@ -18,8 +19,15 @@ import DeleteOutlineRoundedIcon from '@mui/icons-material/DeleteOutlineRounded'; import Tooltip from '@mui/material/Tooltip'; import Badge from '@mui/material/Badge'; +import Divider from '@mui/material/Divider'; 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 userSettingsAutomat from '../../automats/userSettingsAutomat'; import ChatFlowList from './ChatFlowList'; @@ -62,6 +70,10 @@ export default class ChatDrawer extends PureComponent { attachmentError: null, drawerWidth: settings?.chatDrawerWidth || DEFAULT_DRAWER_WIDTH, isResizing: false, + showSettingsBar: false, + modelslugflag: 'floor', + modelreasoning: 'medium', + modelslug: 'openai/gpt-6-luna', }; this.inputRef = React.createRef(); this.fileInputRef = React.createRef(); @@ -79,6 +91,7 @@ export default class ChatDrawer extends PureComponent { if (this.state.isOpen) { this.focusInput(150); } + void this.loadModelSettings(); this.unsub = chatAutomat.subscribe(this, (state) => { const wasOpen = this.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); renderHeader() { @@ -345,9 +423,12 @@ export default class ChatDrawer extends PureComponent { conversations, selectedConversationId, loadingConversations, + showSettingsBar, + modelslugflag, + modelreasoning, + modelslug, } = this.state; - return ( - - - - - KI-Assistent - - + + {showSettingsBar ? ( + /* Settings Bar Mode: Title goes away and reveals toggles */ + + + + - + {/* Group 1: Speed (floor, walk/standard "", run/nitro) */} + (theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.05)'), + p: 0.3, + borderRadius: 2, + flexShrink: 0, + }} + > + 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', + }, + }} + > + + + + 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', + }, + }} + > + + + + 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', + }, + }} + > + + + + + + + {/* Group 2: Reasoning (none, low, medium, max) */} + (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) */} + 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', + }, + }} + > + + + + + + + {/* low toggle (smallest brain) */} + 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', + }, + }} + > + + + + {/* medium toggle (medium brain) */} + 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', + }, + }} + > + + + + {/* max toggle (big brain) */} + 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', + }, + }} + > + + + + + + + {/* modelslug input */} + (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', + }, + }} + /> + + ) : ( + /* Normal Mode: settings icon on the left end, title visible */ + + + + + + + KI-Assistent + + + )} + + diff --git a/src/components/settings/SettingsKeysPanel.jsx b/src/components/settings/SettingsKeysPanel.jsx index a14f25d..a1bb202 100644 --- a/src/components/settings/SettingsKeysPanel.jsx +++ b/src/components/settings/SettingsKeysPanel.jsx @@ -3,6 +3,7 @@ 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'; @@ -28,7 +29,7 @@ const SETTABLE_KEYS = [ 'keys.tavily', 'keys.openrouter', 'keys.serper', - 'default-model', + 'modelslugimage', 'systemprompt-prefix', 'systemprompt-suffix', ]; @@ -38,11 +39,17 @@ const KEY_LABELS = { 'keys.tavily': 'Tavily API-Key', 'keys.openrouter': 'OpenRouter API-Key', 'keys.serper': 'Serper API-Key', - 'default-model': 'Modellslug', + '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', @@ -80,12 +87,17 @@ export default class SettingsKeysPanel extends Component { try { const data = await settingApi.list(); 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( settings.map((s) => [ s.key, { - value: s.value ?? '', + value: s.value ?? (DEFAULT_SETTINGS[s.key] ?? ''), busy: false, saveError: null, }, @@ -117,14 +129,8 @@ export default class SettingsKeysPanel extends Component { const row = this.state.settings.find((s) => s.key === key); if (!row) return; - const body = {}; - if (draft.value !== (row.value ?? '')) { - body.value = draft.value; - } - - if (Object.keys(body).length === 0) { - return; - } + const valToSave = draft.value !== '' ? draft.value : (DEFAULT_SETTINGS[key] ?? ''); + const body = { value: valToSave }; this.setState((prev) => ({ drafts: { @@ -146,7 +152,7 @@ export default class SettingsKeysPanel extends Component { drafts: { ...this.state.drafts, [key]: { - value: updated.value ?? '', + value: updated.value ?? valToSave, busy: false, saveError: null, }, @@ -198,6 +204,7 @@ export default class SettingsKeysPanel extends Component { 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)} - this.handleChange(row.key, 'value', e.target.value)} - onKeyDown={(e) => { - if (!isMultiline && e.key === 'Enter') { - e.preventDefault(); - void this.handleSave(row.key); - } - }} - autoComplete="off" - /> + {enumOpts ? ( + this.handleChange(row.key, 'value', e.target.value)} + slotProps={{ htmlInput: { spellCheck: false } }} + inputProps={{ 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' } }} + inputProps={{ spellCheck: false, autoCorrect: 'off', autoCapitalize: 'off' }} + /> + )} {brand?.name ? ( @@ -310,9 +312,9 @@ export class ArticleTile extends Tile { {sku} ) : null} - {article.net != null && ( + {grossPrice != null && ( - {formatPrice(article.net)} + {formatPrice(grossPrice)} )} @@ -353,6 +355,7 @@ export class ArticleTile extends Tile { const hasPhoto = this.hasPhoto(); const { article, label, sx, disabled } = this.props; const sku = articleSku(article); + const grossPrice = getArticleGross(article, companyAutomat.state.current?.taxrate); return ( ) : null} - {article.net != null && ( + {grossPrice != null && ( - {formatPrice(article.net)} + {formatPrice(grossPrice)} )} diff --git a/src/routes/artikel.jsx b/src/routes/artikel.jsx index b0b820b..730262d 100644 --- a/src/routes/artikel.jsx +++ b/src/routes/artikel.jsx @@ -20,7 +20,7 @@ import ExpandLessIcon from '@mui/icons-material/ExpandLess'; import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; import articleSearchAutomat from '../automats/articleSearchAutomat'; import BackendError from '../components/BackendError'; -import { formatPrice } from '../utils/price'; +import { formatPrice, getArticleGross } from '../utils/price'; import { buildSearchUrl, queryFromParams, @@ -46,6 +46,7 @@ import { setLastCategoryId, navigationAutomat, categoryAutomat, + companyAutomat, } from '../automats'; import { BrandLabel } from '../components/brand'; import { SCRIM_LABEL } from '../colors'; @@ -586,7 +587,7 @@ class ArtikelSearchPage extends Component { const isParent = Boolean(row.isParent || row.is_parent); const childCount = Number(row.childCount) || 0; const showChildBadge = isParent || childCount > 0; - const price = row.price ?? row.net ?? row.vknetto; + const price = getArticleGross(row, companyAutomat.state.current?.taxrate); return ( 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; +} +