mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 12:17:45 +02:00
[eric] split: extract Settings general + models tabs, thin parent shell
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Button from '@mui/material/Button';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
const ConfirmDiscardDialog: React.FC<{ open: boolean; onCancel: () => void; onConfirm: () => void }> = ({ open, onCancel, onConfirm }) => {
|
||||
const c = useClaudeTokens();
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onCancel}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
bgcolor: c.bg.page,
|
||||
borderRadius: 2,
|
||||
border: `1px solid ${c.border.subtle}`,
|
||||
boxShadow: c.shadow.md,
|
||||
maxWidth: 380,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle sx={{ color: c.text.primary, fontWeight: 600, fontSize: '0.95rem', pb: 0.5, px: 3, pt: 2.5 }}>
|
||||
Discard unsaved changes?
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ px: 3 }}>
|
||||
<Typography sx={{ color: c.text.muted, fontSize: '0.85rem' }}>
|
||||
Your in-progress edits will be cleared and the form will revert to your saved settings. This can't be undone.
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2, gap: 1 }}>
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
sx={{ color: c.text.muted, textTransform: 'none', fontSize: '0.85rem' }}
|
||||
>
|
||||
Keep editing
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={onConfirm}
|
||||
sx={{
|
||||
bgcolor: c.status.error,
|
||||
color: '#fff',
|
||||
'&:hover': { bgcolor: c.status.error, filter: 'brightness(0.9)' },
|
||||
textTransform: 'none',
|
||||
borderRadius: 1.5,
|
||||
fontSize: '0.85rem',
|
||||
}}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmDiscardDialog;
|
||||
@@ -0,0 +1,113 @@
|
||||
import React from 'react';
|
||||
import { report } from '@/shared/serviceClient';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Button from '@mui/material/Button';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { closeSettingsModal, AppSettings } from '@/shared/state/settingsSlice';
|
||||
import { onboardingBus } from '@/app/components/Onboarding/eventBus';
|
||||
import { resetTour } from '@/app/components/Onboarding/OnboardingProgressSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import TrustedFilePatterns from '@/app/components/TrustedFilePatterns';
|
||||
import SoftwareUpdateRow from './SoftwareUpdateRow';
|
||||
import type { SettingsStyles } from './settingsStyles';
|
||||
|
||||
const GeneralAdvanced: React.FC<{
|
||||
form: AppSettings;
|
||||
setForm: React.Dispatch<React.SetStateAction<AppSettings>>;
|
||||
styles: SettingsStyles;
|
||||
}> = ({ form, setForm, styles }) => {
|
||||
const c = useClaudeTokens();
|
||||
const dispatch = useAppDispatch();
|
||||
const appVersion = useAppSelector((s) => s.update.appVersion);
|
||||
const { sectionSx, rowSx, inlineRowSx, inlineRowLastSx, labelSx, descSx } = styles;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography sx={{ ...sectionSx, mt: 3 }}>Advanced</Typography>
|
||||
|
||||
<Box sx={inlineRowSx}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Developer mode</Typography>
|
||||
<Typography sx={descSx}>Show transport details, environment variables, raw configs, and other technical metadata throughout the app.</Typography>
|
||||
</Box>
|
||||
<Switch
|
||||
checked={form.dev_mode}
|
||||
onChange={(e) => setForm({ ...form, dev_mode: e.target.checked })}
|
||||
sx={{
|
||||
'& .MuiSwitch-switchBase.Mui-checked': { color: c.accent.primary },
|
||||
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { bgcolor: c.accent.primary },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowLastSx}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Experimental updates</Typography>
|
||||
<Typography sx={descSx}>Receive pre-release builds with new features earlier. These versions may be less stable than normal releases.</Typography>
|
||||
</Box>
|
||||
<Switch
|
||||
checked={form.allow_experimental_updates}
|
||||
onChange={(e) => setForm({ ...form, allow_experimental_updates: e.target.checked })}
|
||||
sx={{
|
||||
'& .MuiSwitch-switchBase.Mui-checked': { color: c.accent.primary },
|
||||
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { bgcolor: c.accent.primary },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Typography sx={{ ...sectionSx, mt: 3 }}>About</Typography>
|
||||
|
||||
<Box sx={rowSx}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box>
|
||||
<Typography sx={labelSx}>Version</Typography>
|
||||
<Typography sx={{ ...descSx, fontFamily: c.font.mono }}>
|
||||
{appVersion ?? '-'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<SoftwareUpdateRow styles={styles} />
|
||||
|
||||
<TrustedFilePatterns />
|
||||
|
||||
<Box sx={{ mt: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box>
|
||||
<Typography sx={{ ...labelSx, mb: 0.25 }}>Onboarding tour</Typography>
|
||||
<Typography sx={{ ...descSx, mb: 0 }}>
|
||||
Re-run the Show me walkthrough at any time.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
data-onboarding="settings-restart-tour"
|
||||
onClick={() => {
|
||||
report('onboarding_v2', 'tour_restarted');
|
||||
try {
|
||||
window.localStorage.removeItem('openswarm.onboarding.v2');
|
||||
} catch { /* ignore */ }
|
||||
dispatch(resetTour());
|
||||
dispatch(closeSettingsModal());
|
||||
onboardingBus.emit('settings:closed');
|
||||
}}
|
||||
sx={{
|
||||
color: c.text.secondary,
|
||||
borderColor: c.border.medium,
|
||||
textTransform: 'none',
|
||||
fontSize: '0.8rem',
|
||||
whiteSpace: 'nowrap',
|
||||
'&:hover': { color: c.accent.primary, borderColor: c.accent.primary },
|
||||
}}
|
||||
>
|
||||
Restart tour
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GeneralAdvanced;
|
||||
@@ -0,0 +1,261 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Button from '@mui/material/Button';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import Select from '@mui/material/Select';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import FolderOpenIcon from '@mui/icons-material/FolderOpen';
|
||||
import RestartAltIcon from '@mui/icons-material/RestartAlt';
|
||||
import { useAppDispatch } from '@/shared/hooks';
|
||||
import { resetSystemPrompt, AppSettings, DEFAULT_SYSTEM_PROMPT } from '@/shared/state/settingsSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import type { SettingsStyles } from './settingsStyles';
|
||||
|
||||
type ModelOption = { value: string; label: string };
|
||||
|
||||
const GeneralAgentDefaults: React.FC<{
|
||||
form: AppSettings;
|
||||
setForm: React.Dispatch<React.SetStateAction<AppSettings>>;
|
||||
styles: SettingsStyles;
|
||||
setBrowseOpen: (v: boolean) => void;
|
||||
modelOptions: { grouped: Record<string, ModelOption[]>; flat: Array<ModelOption & { provider: string }> };
|
||||
modesList: Array<{ id: string; name: string }>;
|
||||
providerColors: Record<string, string>;
|
||||
openswarmGradient: string;
|
||||
}> = ({ form, setForm, styles, setBrowseOpen, modelOptions, modesList, providerColors, openswarmGradient }) => {
|
||||
const c = useClaudeTokens();
|
||||
const dispatch = useAppDispatch();
|
||||
const { fieldSx, sectionSx, rowSx, inlineRowSx, inlineRowLastSx, labelSx, descSx } = styles;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography sx={sectionSx}>Agent Defaults</Typography>
|
||||
|
||||
<Box sx={rowSx}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 0.5 }}>
|
||||
<Typography sx={labelSx}>System prompt</Typography>
|
||||
{form.default_system_prompt !== DEFAULT_SYSTEM_PROMPT && (
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<RestartAltIcon sx={{ fontSize: 14 }} />}
|
||||
onClick={async () => {
|
||||
await dispatch(resetSystemPrompt());
|
||||
setForm((prev) => ({ ...prev, default_system_prompt: DEFAULT_SYSTEM_PROMPT }));
|
||||
}}
|
||||
sx={{
|
||||
color: c.accent.primary,
|
||||
textTransform: 'none',
|
||||
fontSize: '0.75rem',
|
||||
py: 0.25,
|
||||
'&:hover': { bgcolor: `${c.accent.primary}10` },
|
||||
}}
|
||||
>
|
||||
Reset to default
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
<Typography sx={{ ...descSx, mb: 1.5 }}>
|
||||
Prepended to every agent session before mode-specific instructions. Modes can override with their own.
|
||||
</Typography>
|
||||
<TextField
|
||||
value={form.default_system_prompt ?? DEFAULT_SYSTEM_PROMPT}
|
||||
onChange={(e) => setForm({ ...form, default_system_prompt: e.target.value || null })}
|
||||
multiline
|
||||
minRows={3}
|
||||
maxRows={8}
|
||||
fullWidth
|
||||
size="small"
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
fontFamily: c.font.mono,
|
||||
fontSize: '0.8rem',
|
||||
lineHeight: 1.6,
|
||||
color: c.text.secondary,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={rowSx}>
|
||||
<Typography sx={labelSx}>Working directory</Typography>
|
||||
<Typography sx={{ ...descSx, mb: 1.5 }}>
|
||||
Default folder agents start in. Modes can override per-mode.
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<TextField
|
||||
value={form.default_folder ?? ''}
|
||||
onChange={(e) => setForm({ ...form, default_folder: e.target.value || null })}
|
||||
size="small"
|
||||
fullWidth
|
||||
placeholder="Not set (uses project root)"
|
||||
sx={{
|
||||
...fieldSx,
|
||||
'& .MuiOutlinedInput-root': {
|
||||
...fieldSx['& .MuiOutlinedInput-root'],
|
||||
fontFamily: c.font.mono,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => setBrowseOpen(true)}
|
||||
startIcon={<FolderOpenIcon sx={{ fontSize: 16 }} />}
|
||||
sx={{
|
||||
color: c.text.tertiary,
|
||||
borderColor: c.border.medium,
|
||||
textTransform: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
minWidth: 'auto',
|
||||
fontSize: '0.8rem',
|
||||
'&:hover': { color: c.accent.primary, borderColor: c.accent.primary },
|
||||
}}
|
||||
>
|
||||
Browse
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowSx}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Model</Typography>
|
||||
<Typography sx={descSx}>Default model for new sessions.</Typography>
|
||||
</Box>
|
||||
<FormControl size="small" sx={{ minWidth: 220 }}>
|
||||
<Select
|
||||
value={form.default_model}
|
||||
onChange={(e) => setForm({ ...form, default_model: e.target.value })}
|
||||
sx={{ fontSize: '0.85rem' }}
|
||||
MenuProps={{ PaperProps: { sx: { bgcolor: c.bg.surface, color: c.text.primary } } }}
|
||||
renderValue={(val) => {
|
||||
const m = modelOptions.flat.find((x) => x.value === val);
|
||||
if (!m) return String(val);
|
||||
return (
|
||||
<Box component="span" sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.75 }}>
|
||||
<span>{m.label}</span>
|
||||
<Typography component="span" sx={{ fontSize: '0.65rem', color: c.text.ghost }}>
|
||||
· {m.provider}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}}
|
||||
>
|
||||
{Object.entries(modelOptions.grouped).flatMap(([prov, models]) => {
|
||||
const isOpenSwarmPro = prov === 'OpenSwarm Pro';
|
||||
const brandColor = providerColors[prov.toLowerCase()] ?? c.text.tertiary;
|
||||
return [
|
||||
<ListSubheader
|
||||
key={`header-${prov}`}
|
||||
sx={{
|
||||
bgcolor: c.bg.surface,
|
||||
lineHeight: '1.8em',
|
||||
px: 1.5,
|
||||
py: 0.4,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: '50%',
|
||||
flexShrink: 0,
|
||||
background: isOpenSwarmPro ? openswarmGradient : brandColor,
|
||||
boxShadow: isOpenSwarmPro
|
||||
? '0 0 8px rgba(229, 107, 196, 0.6)'
|
||||
: `0 0 6px ${brandColor}80`,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.68rem',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.08em',
|
||||
textTransform: 'uppercase',
|
||||
...(isOpenSwarmPro
|
||||
? {
|
||||
background: openswarmGradient,
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
backgroundClip: 'text',
|
||||
}
|
||||
: { color: brandColor }),
|
||||
}}
|
||||
>
|
||||
{prov}
|
||||
</Typography>
|
||||
</Box>
|
||||
</ListSubheader>,
|
||||
...models.map((m) => (
|
||||
<MenuItem key={m.value} value={m.value} sx={{ fontSize: '0.85rem', pl: 3 }}>
|
||||
{m.label}
|
||||
</MenuItem>
|
||||
)),
|
||||
];
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowSx}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Mode</Typography>
|
||||
<Typography sx={descSx}>Default interaction mode for new sessions.</Typography>
|
||||
</Box>
|
||||
<FormControl size="small" sx={{ minWidth: 170 }}>
|
||||
<Select
|
||||
value={form.default_mode}
|
||||
onChange={(e) => setForm({ ...form, default_mode: e.target.value })}
|
||||
sx={{ fontSize: '0.85rem' }}
|
||||
MenuProps={{ PaperProps: { sx: { bgcolor: c.bg.surface, color: c.text.primary } } }}
|
||||
>
|
||||
{modesList.map((m) => (
|
||||
<MenuItem key={m.id} value={m.id}>{m.name}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowSx}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Thinking</Typography>
|
||||
<Typography sx={descSx}>Default thinking level for reasoning-capable models.</Typography>
|
||||
</Box>
|
||||
<FormControl size="small" sx={{ minWidth: 170 }}>
|
||||
<Select
|
||||
value={form.default_thinking_level}
|
||||
onChange={(e) => setForm({ ...form, default_thinking_level: e.target.value as AppSettings['default_thinking_level'] })}
|
||||
sx={{ fontSize: '0.85rem' }}
|
||||
MenuProps={{ PaperProps: { sx: { bgcolor: c.bg.surface, color: c.text.primary } } }}
|
||||
>
|
||||
<MenuItem value="auto">Auto</MenuItem>
|
||||
<MenuItem value="off">Off</MenuItem>
|
||||
<MenuItem value="low">Low</MenuItem>
|
||||
<MenuItem value="medium">Medium</MenuItem>
|
||||
<MenuItem value="high">High</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowLastSx}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Max turns</Typography>
|
||||
<Typography sx={descSx}>Auto-stop after this many turns. Empty = unlimited.</Typography>
|
||||
</Box>
|
||||
<TextField
|
||||
type="number"
|
||||
value={form.default_max_turns ?? ''}
|
||||
onChange={(e) => setForm({ ...form, default_max_turns: e.target.value ? parseInt(e.target.value) : null })}
|
||||
size="small"
|
||||
placeholder="∞"
|
||||
inputProps={{ min: 1 }}
|
||||
sx={{ ...fieldSx, width: 100 }}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GeneralAgentDefaults;
|
||||
@@ -0,0 +1,226 @@
|
||||
import React, { useState } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import ToggleButton from '@mui/material/ToggleButton';
|
||||
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
||||
import Slider from '@mui/material/Slider';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import LightModeIcon from '@mui/icons-material/LightMode';
|
||||
import DarkModeIcon from '@mui/icons-material/DarkMode';
|
||||
import KeyboardIcon from '@mui/icons-material/Keyboard';
|
||||
import LanguageIcon from '@mui/icons-material/Language';
|
||||
import { AppSettings } from '@/shared/state/settingsSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import type { SettingsStyles } from './settingsStyles';
|
||||
|
||||
const GeneralInterface: React.FC<{
|
||||
form: AppSettings;
|
||||
setForm: React.Dispatch<React.SetStateAction<AppSettings>>;
|
||||
styles: SettingsStyles;
|
||||
}> = ({ form, setForm, styles }) => {
|
||||
const c = useClaudeTokens();
|
||||
const [recordingShortcut, setRecordingShortcut] = useState(false);
|
||||
const { fieldSx, sectionSx, rowSx, rowLastSx, inlineRowSx, inlineRowLastSx, labelSx, descSx } = styles;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography sx={{ ...sectionSx, mt: 3 }}>Interface</Typography>
|
||||
|
||||
<Box sx={inlineRowSx}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Theme</Typography>
|
||||
<Typography sx={descSx}>Application color scheme.</Typography>
|
||||
</Box>
|
||||
<ToggleButtonGroup
|
||||
value={form.theme}
|
||||
exclusive
|
||||
onChange={(_, v) => { if (v) setForm({ ...form, theme: v }); }}
|
||||
size="small"
|
||||
sx={{
|
||||
'& .MuiToggleButton-root': {
|
||||
color: c.text.muted,
|
||||
borderColor: c.border.medium,
|
||||
textTransform: 'none',
|
||||
px: 2,
|
||||
py: 0.5,
|
||||
gap: 0.5,
|
||||
fontSize: '0.8rem',
|
||||
'&.Mui-selected': {
|
||||
bgcolor: `${c.accent.primary}15`,
|
||||
color: c.accent.primary,
|
||||
borderColor: c.accent.primary,
|
||||
'&:hover': { bgcolor: `${c.accent.primary}20` },
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="light">
|
||||
<LightModeIcon sx={{ fontSize: 16 }} /> Light
|
||||
</ToggleButton>
|
||||
<ToggleButton value="dark">
|
||||
<DarkModeIcon sx={{ fontSize: 16 }} /> Dark
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
|
||||
<Box sx={rowSx}>
|
||||
<Typography sx={labelSx}>Zoom sensitivity</Typography>
|
||||
<Typography sx={{ ...descSx, mb: 1 }}>
|
||||
Scroll-to-zoom responsiveness. Lower for trackpads, higher for mouse wheels.
|
||||
</Typography>
|
||||
<Box sx={{ px: 1 }}>
|
||||
<Slider
|
||||
value={form.zoom_sensitivity}
|
||||
onChange={(_, v) => setForm({ ...form, zoom_sensitivity: v as number })}
|
||||
min={1}
|
||||
max={100}
|
||||
step={1}
|
||||
valueLabelDisplay="auto"
|
||||
marks={[
|
||||
{ value: 1, label: 'Low' },
|
||||
{ value: 50, label: 'Default' },
|
||||
{ value: 100, label: 'High' },
|
||||
]}
|
||||
sx={{
|
||||
color: c.accent.primary,
|
||||
'& .MuiSlider-markLabel': { color: c.text.tertiary, fontSize: '0.7rem' },
|
||||
'& .MuiSlider-valueLabel': { bgcolor: c.accent.primary },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowSx}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>New agent shortcut</Typography>
|
||||
<Typography sx={descSx}>Keyboard shortcut to create an agent.</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (!recordingShortcut) return;
|
||||
if (['Meta', 'Control', 'Shift', 'Alt'].includes(e.key)) return;
|
||||
e.preventDefault();
|
||||
const parts: string[] = [];
|
||||
if (e.metaKey) parts.push('Meta');
|
||||
if (e.ctrlKey) parts.push('Ctrl');
|
||||
if (e.altKey) parts.push('Alt');
|
||||
if (e.shiftKey) parts.push('Shift');
|
||||
parts.push(e.key.length === 1 ? e.key.toLowerCase() : e.key);
|
||||
setForm({ ...form, new_agent_shortcut: parts.join('+') });
|
||||
setRecordingShortcut(false);
|
||||
}}
|
||||
onBlur={() => setRecordingShortcut(false)}
|
||||
onClick={() => setRecordingShortcut(true)}
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.75,
|
||||
px: 1.5,
|
||||
py: 0.75,
|
||||
borderRadius: `${c.radius.sm}px`,
|
||||
border: `1px solid ${recordingShortcut ? c.accent.primary : c.border.medium}`,
|
||||
cursor: 'pointer',
|
||||
outline: 'none',
|
||||
transition: 'border-color 0.15s',
|
||||
'&:hover': { borderColor: c.accent.primary },
|
||||
}}
|
||||
>
|
||||
<KeyboardIcon sx={{ fontSize: 16, color: recordingShortcut ? c.accent.primary : c.text.tertiary }} />
|
||||
{recordingShortcut ? (
|
||||
<Typography sx={{ fontSize: '0.8rem', color: c.accent.primary, fontWeight: 500 }}>
|
||||
Press shortcut…
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography sx={{ fontSize: '0.8rem', color: c.text.primary, fontFamily: c.font.mono, fontWeight: 500 }}>
|
||||
{form.new_agent_shortcut
|
||||
.split('+')
|
||||
.map((p) => {
|
||||
if (p === 'Meta') return '⌘';
|
||||
if (p === 'Ctrl') return 'Ctrl';
|
||||
if (p === 'Alt') return '⌥';
|
||||
if (p === 'Shift') return '⇧';
|
||||
return p.toUpperCase();
|
||||
})
|
||||
.join(' + ')}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowSx}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Auto-enable element selection</Typography>
|
||||
<Typography sx={descSx}>Automatically enter element selection mode when creating a new agent.</Typography>
|
||||
</Box>
|
||||
<Switch
|
||||
checked={form.auto_select_mode_on_new_agent}
|
||||
onChange={(e) => setForm({ ...form, auto_select_mode_on_new_agent: e.target.checked })}
|
||||
sx={{
|
||||
'& .MuiSwitch-switchBase.Mui-checked': { color: c.accent.primary },
|
||||
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { bgcolor: c.accent.primary },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowSx}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Default agent spawn state in dashboard</Typography>
|
||||
<Typography sx={descSx}>When enabled, new agents spawn expanded instead of collapsed.</Typography>
|
||||
</Box>
|
||||
<Switch
|
||||
checked={form.expand_new_chats_in_dashboard}
|
||||
onChange={(e) => setForm({ ...form, expand_new_chats_in_dashboard: e.target.checked })}
|
||||
sx={{
|
||||
'& .MuiSwitch-switchBase.Mui-checked': { color: c.accent.primary },
|
||||
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { bgcolor: c.accent.primary },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowLastSx}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Auto-reveal sub-agents on dashboard</Typography>
|
||||
<Typography sx={descSx}>Automatically show sub-agent cards (from CreateAgent / InvokeAgent) tethered to their parent on the dashboard.</Typography>
|
||||
</Box>
|
||||
<Switch
|
||||
checked={form.auto_reveal_sub_agents}
|
||||
onChange={(e) => setForm({ ...form, auto_reveal_sub_agents: e.target.checked })}
|
||||
sx={{
|
||||
'& .MuiSwitch-switchBase.Mui-checked': { color: c.accent.primary },
|
||||
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { bgcolor: c.accent.primary },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Typography sx={{ ...sectionSx, mt: 3 }}>Browser</Typography>
|
||||
|
||||
<Box sx={rowLastSx}>
|
||||
<Typography sx={labelSx}>Default homepage</Typography>
|
||||
<Typography sx={{ ...descSx, mb: 1.5 }}>
|
||||
URL loaded when opening a new browser card on the dashboard.
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||
<LanguageIcon sx={{ fontSize: 18, color: c.text.tertiary, flexShrink: 0 }} />
|
||||
<TextField
|
||||
value={form.browser_homepage}
|
||||
onChange={(e) => setForm({ ...form, browser_homepage: e.target.value })}
|
||||
size="small"
|
||||
fullWidth
|
||||
placeholder="https://www.google.com"
|
||||
sx={{
|
||||
...fieldSx,
|
||||
'& .MuiOutlinedInput-root': {
|
||||
...fieldSx['& .MuiOutlinedInput-root'],
|
||||
fontFamily: c.font.mono,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GeneralInterface;
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { AppSettings } from '@/shared/state/settingsSlice';
|
||||
import AccountCard from './AccountCard';
|
||||
import GeneralAgentDefaults from './GeneralAgentDefaults';
|
||||
import GeneralInterface from './GeneralInterface';
|
||||
import GeneralAdvanced from './GeneralAdvanced';
|
||||
import type { SettingsStyles } from './settingsStyles';
|
||||
|
||||
type ModelOption = { value: string; label: string };
|
||||
|
||||
const GeneralTab: React.FC<{
|
||||
form: AppSettings;
|
||||
setForm: React.Dispatch<React.SetStateAction<AppSettings>>;
|
||||
styles: SettingsStyles;
|
||||
setBrowseOpen: (v: boolean) => void;
|
||||
modelOptions: { grouped: Record<string, ModelOption[]>; flat: Array<ModelOption & { provider: string }> };
|
||||
modesList: Array<{ id: string; name: string }>;
|
||||
providerColors: Record<string, string>;
|
||||
openswarmGradient: string;
|
||||
}> = ({ form, setForm, styles, setBrowseOpen, modelOptions, modesList, providerColors, openswarmGradient }) => {
|
||||
const { sectionSx } = styles;
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', pt: 2.5, pb: 1, animation: 'fadeIn 0.2s ease', '@keyframes fadeIn': { from: { opacity: 0 }, to: { opacity: 1 } } }}>
|
||||
|
||||
<Typography sx={sectionSx}>Account</Typography>
|
||||
<AccountCard />
|
||||
|
||||
<GeneralAgentDefaults
|
||||
form={form}
|
||||
setForm={setForm}
|
||||
styles={styles}
|
||||
setBrowseOpen={setBrowseOpen}
|
||||
modelOptions={modelOptions}
|
||||
modesList={modesList}
|
||||
providerColors={providerColors}
|
||||
openswarmGradient={openswarmGradient}
|
||||
/>
|
||||
|
||||
<GeneralInterface form={form} setForm={setForm} styles={styles} />
|
||||
|
||||
<GeneralAdvanced form={form} setForm={setForm} styles={styles} />
|
||||
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GeneralTab;
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { AppSettings } from '@/shared/state/settingsSlice';
|
||||
import OpenSwarmProCard from './OpenSwarmProCard';
|
||||
import SubscriptionCards from './SubscriptionCards';
|
||||
import ApiKeyCard, { API_KEY_CARDS } from './ApiKeyCard';
|
||||
import CustomProvidersEditor from './CustomProvidersEditor';
|
||||
import type { SettingsStyles } from './settingsStyles';
|
||||
|
||||
const ModelsTab: React.FC<{
|
||||
form: AppSettings;
|
||||
setForm: React.Dispatch<React.SetStateAction<AppSettings>>;
|
||||
showApiKey: boolean;
|
||||
setShowApiKey: (v: boolean) => void;
|
||||
styles: SettingsStyles;
|
||||
}> = ({ form, setForm, showApiKey, setShowApiKey, styles }) => {
|
||||
const c = useClaudeTokens();
|
||||
const { descSx } = styles;
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', pt: 2.5, pb: 1, gap: 2.5, animation: 'fadeIn 0.2s ease', '@keyframes fadeIn': { from: { opacity: 0 }, to: { opacity: 1 } } }}>
|
||||
|
||||
<Box data-onboarding="settings-pro-section" sx={{ display: 'flex', flexDirection: 'column', gap: 2.5 }}>
|
||||
<Typography sx={{ fontSize: '0.7rem', color: c.text.ghost, textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 600 }}>
|
||||
One Subscription, No Setup
|
||||
</Typography>
|
||||
|
||||
<Typography sx={{ ...descSx, mb: 0 }}>
|
||||
Don't have a Claude account? We'll handle it for you. One simple subscription covers Claude Sonnet, Opus, and Haiku.
|
||||
</Typography>
|
||||
|
||||
<OpenSwarmProCard />
|
||||
</Box>
|
||||
|
||||
<Box data-onboarding="settings-external-subs" sx={{ display: 'flex', flexDirection: 'column', gap: 2.5 }}>
|
||||
<Typography sx={{ fontSize: '0.7rem', color: c.text.ghost, textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 600, mt: 1 }}>
|
||||
Or Use Your Existing Subscriptions
|
||||
</Typography>
|
||||
|
||||
<Typography sx={{ ...descSx, mb: 0 }}>
|
||||
Already paying for Claude, ChatGPT, or Gemini? Connect your subscription, no API key needed, no extra cost.
|
||||
</Typography>
|
||||
|
||||
<SubscriptionCards />
|
||||
</Box>
|
||||
|
||||
<Box data-onboarding="settings-api-keys" sx={{ display: 'flex', flexDirection: 'column', gap: 2.5 }}>
|
||||
<Typography sx={{ fontSize: '0.7rem', color: c.text.ghost, textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 600, mt: 1 }}>
|
||||
Or Connect With API Keys
|
||||
</Typography>
|
||||
|
||||
<Typography sx={{ ...descSx, mb: -1 }}>
|
||||
Pay per use. Each key is stored locally on your device.
|
||||
</Typography>
|
||||
|
||||
{API_KEY_CARDS.map((config) => (
|
||||
<ApiKeyCard
|
||||
key={config.field}
|
||||
config={config}
|
||||
form={form}
|
||||
setForm={setForm}
|
||||
showApiKey={showApiKey}
|
||||
setShowApiKey={setShowApiKey}
|
||||
styles={styles}
|
||||
/>
|
||||
))}
|
||||
|
||||
<CustomProvidersEditor
|
||||
form={form}
|
||||
setForm={setForm}
|
||||
showApiKey={showApiKey}
|
||||
setShowApiKey={setShowApiKey}
|
||||
styles={styles}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelsTab;
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
const SettingsFooter: React.FC<{
|
||||
hasChanges: boolean;
|
||||
onDiscard: () => void;
|
||||
onClose: () => void;
|
||||
onSave: () => void;
|
||||
}> = ({ hasChanges, onDiscard, onClose, onSave }) => {
|
||||
const c = useClaudeTokens();
|
||||
return (
|
||||
<DialogActions sx={{ borderTop: `1px solid ${c.border.subtle}`, px: 3, py: 1.5, justifyContent: 'space-between' }}>
|
||||
{/* Left: explicit Discard; only path to wipe the persisted draft. */}
|
||||
<Box>
|
||||
{hasChanges && (
|
||||
<Button
|
||||
onClick={onDiscard}
|
||||
sx={{ color: c.status.error, textTransform: 'none', fontSize: '0.85rem' }}
|
||||
>
|
||||
Discard changes
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
sx={{ color: c.text.muted, textTransform: 'none', fontSize: '0.85rem' }}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<SaveIcon sx={{ fontSize: 16 }} />}
|
||||
onClick={onSave}
|
||||
disabled={!hasChanges}
|
||||
sx={{
|
||||
bgcolor: c.accent.primary,
|
||||
'&:hover': { bgcolor: c.accent.pressed },
|
||||
'&.Mui-disabled': { bgcolor: c.bg.secondary, color: c.text.ghost },
|
||||
textTransform: 'none',
|
||||
borderRadius: 1.5,
|
||||
px: 2.5,
|
||||
fontSize: '0.85rem',
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Box>
|
||||
</DialogActions>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsFooter;
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
const SettingsHeader: React.FC<{
|
||||
activeTab: string;
|
||||
onTabChange: (v: any) => void;
|
||||
onClose: () => void;
|
||||
}> = ({ activeTab, onTabChange, onClose }) => {
|
||||
const c = useClaudeTokens();
|
||||
return (
|
||||
<DialogTitle
|
||||
sx={{
|
||||
px: 3,
|
||||
py: 0,
|
||||
borderBottom: `1px solid ${c.border.subtle}`,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', pt: 1.5, pb: 0.5 }}>
|
||||
<Typography sx={{ color: c.text.primary, fontWeight: 600, fontSize: '1rem' }}>
|
||||
Settings
|
||||
</Typography>
|
||||
<IconButton onClick={onClose} size="small" data-onboarding="settings-close-button" sx={{ color: c.text.tertiary, '&:hover': { color: c.text.primary } }}>
|
||||
<CloseIcon sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(_, v) => onTabChange(v)}
|
||||
sx={{
|
||||
minHeight: 36,
|
||||
'& .MuiTab-root': {
|
||||
minHeight: 36,
|
||||
textTransform: 'none',
|
||||
fontSize: '0.85rem',
|
||||
fontWeight: 500,
|
||||
color: c.text.muted,
|
||||
px: 1.5,
|
||||
'&.Mui-selected': { color: c.accent.primary, fontWeight: 600 },
|
||||
},
|
||||
'& .MuiTabs-indicator': { backgroundColor: c.accent.primary, height: 2 },
|
||||
}}
|
||||
>
|
||||
<Tab label="General" value="general" disableRipple />
|
||||
<Tab label="Models" value="models" disableRipple data-onboarding="settings-models-tab" />
|
||||
<Tab label="Usage" value="usage" disableRipple />
|
||||
<Tab label="Commands" value="commands" disableRipple />
|
||||
</Tabs>
|
||||
</DialogTitle>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsHeader;
|
||||
@@ -0,0 +1,156 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Button from '@mui/material/Button';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||
import RestartAltIcon from '@mui/icons-material/RestartAlt';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { setChecking, setUpdateError, setInstalling } from '@/shared/state/updateSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import type { SettingsStyles } from './settingsStyles';
|
||||
|
||||
const SoftwareUpdateRow: React.FC<{ styles: SettingsStyles }> = ({ styles }) => {
|
||||
const c = useClaudeTokens();
|
||||
const dispatch = useAppDispatch();
|
||||
const { rowLastSx, labelSx, descSx } = styles;
|
||||
const updateStatus = useAppSelector((s) => s.update.status);
|
||||
const availableVersion = useAppSelector((s) => s.update.availableVersion);
|
||||
const downloadPercent = useAppSelector((s) => s.update.downloadPercent);
|
||||
const updateError = useAppSelector((s) => s.update.error);
|
||||
const installing = useAppSelector((s) => s.update.installing);
|
||||
|
||||
const handleCheckForUpdates = async () => {
|
||||
dispatch(setChecking());
|
||||
const timeout = setTimeout(() => {
|
||||
dispatch(setUpdateError('Update check timed out. Please try again.'));
|
||||
}, 15000);
|
||||
try {
|
||||
await (window as any).openswarm?.checkForUpdates();
|
||||
} catch {
|
||||
/* error handled via IPC event listener */
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadUpdate = async () => {
|
||||
try {
|
||||
await (window as any).openswarm?.downloadUpdate();
|
||||
} catch {
|
||||
/* error handled via IPC event listener */
|
||||
}
|
||||
};
|
||||
|
||||
const handleInstallUpdate = () => {
|
||||
if (installing) return;
|
||||
dispatch(setInstalling());
|
||||
(window as any).openswarm?.installUpdate();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={rowLastSx}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: updateStatus === 'downloading' ? 1 : 0 }}>
|
||||
<Box>
|
||||
<Typography sx={labelSx}>Software update</Typography>
|
||||
<Typography sx={descSx}>
|
||||
{updateStatus === 'checking' && 'Checking for updates…'}
|
||||
{updateStatus === 'not-available' && 'You\'re on the latest version.'}
|
||||
{updateStatus === 'available' && `Version ${availableVersion} is available.`}
|
||||
{updateStatus === 'downloading' && `Downloading update… ${Math.round(downloadPercent)}%`}
|
||||
{updateStatus === 'downloaded' && `Version ${availableVersion} is ready to install.`}
|
||||
{updateStatus === 'error' && (updateError || 'Update check failed.')}
|
||||
{updateStatus === 'idle' && 'Check for new versions of OpenSwarm.'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexShrink: 0, ml: 2 }}>
|
||||
{updateStatus === 'checking' && (
|
||||
<CircularProgress size={18} sx={{ color: c.text.tertiary }} />
|
||||
)}
|
||||
{updateStatus === 'not-available' && (
|
||||
<CheckCircleOutlineIcon sx={{ fontSize: 18, color: c.status.success }} />
|
||||
)}
|
||||
{updateStatus === 'error' && (
|
||||
<ErrorOutlineIcon sx={{ fontSize: 18, color: c.status.error }} />
|
||||
)}
|
||||
{(updateStatus === 'idle' || updateStatus === 'not-available' || updateStatus === 'error') && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={handleCheckForUpdates}
|
||||
startIcon={<SystemUpdateAltIcon sx={{ fontSize: 15 }} />}
|
||||
sx={{
|
||||
color: c.text.secondary,
|
||||
borderColor: c.border.medium,
|
||||
textTransform: 'none',
|
||||
fontSize: '0.8rem',
|
||||
whiteSpace: 'nowrap',
|
||||
'&:hover': { color: c.accent.primary, borderColor: c.accent.primary },
|
||||
}}
|
||||
>
|
||||
Check for Updates
|
||||
</Button>
|
||||
)}
|
||||
{updateStatus === 'available' && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={handleDownloadUpdate}
|
||||
startIcon={<DownloadIcon sx={{ fontSize: 15 }} />}
|
||||
sx={{
|
||||
color: c.accent.primary,
|
||||
borderColor: c.accent.primary,
|
||||
textTransform: 'none',
|
||||
fontSize: '0.8rem',
|
||||
whiteSpace: 'nowrap',
|
||||
'&:hover': { bgcolor: `${c.accent.primary}10` },
|
||||
}}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
)}
|
||||
{updateStatus === 'downloaded' && (
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={handleInstallUpdate}
|
||||
disabled={installing}
|
||||
startIcon={installing
|
||||
? <CircularProgress size={14} sx={{ color: '#fff' }} />
|
||||
: <RestartAltIcon sx={{ fontSize: 15 }} />}
|
||||
sx={{
|
||||
bgcolor: c.accent.primary,
|
||||
'&:hover': { bgcolor: c.accent.pressed },
|
||||
'&.Mui-disabled': { bgcolor: c.accent.primary, color: '#fff', opacity: 0.7 },
|
||||
textTransform: 'none',
|
||||
fontSize: '0.8rem',
|
||||
whiteSpace: 'nowrap',
|
||||
borderRadius: 1.5,
|
||||
}}
|
||||
>
|
||||
{installing ? 'Restarting…' : 'Restart & Update'}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
{updateStatus === 'downloading' && (
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={downloadPercent}
|
||||
sx={{
|
||||
height: 3,
|
||||
borderRadius: 2,
|
||||
bgcolor: `${c.accent.primary}20`,
|
||||
'& .MuiLinearProgress-bar': { bgcolor: c.accent.primary, borderRadius: 2 },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SoftwareUpdateRow;
|
||||
Reference in New Issue
Block a user