mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-13 05:07:40 +02:00
[eric] settings: Appearance keeps only theme/text/accent; Dictation, Canvas, and Agents get their own rail pages
This commit is contained in:
@@ -14,6 +14,9 @@ import { CommandsContent } from '@/app/pages/Commands/Commands';
|
||||
import AccountCard from './sections/subscription/AccountCard';
|
||||
import GeneralAgentDefaults from './sections/general/GeneralAgentDefaults';
|
||||
import GeneralInterface from './sections/general/GeneralInterface';
|
||||
import DictationSettings from './sections/general/DictationSettings';
|
||||
import CanvasSettings from './sections/general/CanvasSettings';
|
||||
import AgentBehaviorSettings from './sections/general/AgentBehaviorSettings';
|
||||
import GeneralAdvanced from './sections/general/GeneralAdvanced';
|
||||
import DataPrivacySection from './sections/general/DataPrivacySection';
|
||||
import ModelsTab from './sections/models/ModelsTab';
|
||||
@@ -29,7 +32,7 @@ import { PROVIDER_COLORS, OPENSWARM_GRADIENT, useModelOptions } from './settings
|
||||
// Module-scope: remember the last open tab across closes (System Settings style).
|
||||
let lastOpenTab: string | null = null;
|
||||
|
||||
const TAB_VALUES = ['account', 'general', 'appearance', 'notifications', 'privacy', 'advanced', 'models', 'commands', 'usage'] as const;
|
||||
const TAB_VALUES = ['account', 'general', 'appearance', 'dictation', 'canvas', 'agents', 'notifications', 'privacy', 'advanced', 'models', 'commands', 'usage'] as const;
|
||||
type SettingsTab = typeof TAB_VALUES[number];
|
||||
const isValidTab = (t: string | null | undefined): t is SettingsTab =>
|
||||
!!t && (TAB_VALUES as readonly string[]).includes(t);
|
||||
@@ -132,6 +135,18 @@ const SettingsBody: React.FC<SettingsBodyProps> = ({ active, onRequestClose }) =
|
||||
<Box sx={{ pt: 0.5, pb: 2, animation: 'fadeIn 0.2s ease', '@keyframes fadeIn': { from: { opacity: 0 }, to: { opacity: 1 } } }}>
|
||||
<GeneralInterface form={form} setForm={setForm} styles={styles} />
|
||||
</Box>
|
||||
) : activeTab === 'dictation' ? (
|
||||
<Box sx={{ pt: 0.5, pb: 2, animation: 'fadeIn 0.2s ease', '@keyframes fadeIn': { from: { opacity: 0 }, to: { opacity: 1 } } }}>
|
||||
<DictationSettings form={form} setForm={setForm} styles={styles} />
|
||||
</Box>
|
||||
) : activeTab === 'canvas' ? (
|
||||
<Box sx={{ pt: 0.5, pb: 2, animation: 'fadeIn 0.2s ease', '@keyframes fadeIn': { from: { opacity: 0 }, to: { opacity: 1 } } }}>
|
||||
<CanvasSettings form={form} setForm={setForm} styles={styles} />
|
||||
</Box>
|
||||
) : activeTab === 'agents' ? (
|
||||
<Box sx={{ pt: 0.5, pb: 2, animation: 'fadeIn 0.2s ease', '@keyframes fadeIn': { from: { opacity: 0 }, to: { opacity: 1 } } }}>
|
||||
<AgentBehaviorSettings form={form} setForm={setForm} styles={styles} />
|
||||
</Box>
|
||||
) : activeTab === 'privacy' ? (
|
||||
<Box sx={{ pt: 0.5, pb: 2, animation: 'fadeIn 0.2s ease', '@keyframes fadeIn': { from: { opacity: 0 }, to: { opacity: 1 } } }}>
|
||||
<DataPrivacySection form={form} setForm={setForm} styles={styles} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { User, Settings2, Palette, ShieldCheck, Wrench, Boxes, SquareSlash, BarChart3, Bell } from 'lucide-react';
|
||||
import { User, Settings2, Palette, ShieldCheck, Wrench, Boxes, SquareSlash, BarChart3, Bell, Mic, LayoutGrid, Bot } from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
@@ -25,6 +25,9 @@ export const RAIL_GROUPS: RailGroup[] = [
|
||||
{ header: 'App', sections: [
|
||||
{ value: 'general', label: 'General', Icon: Settings2 },
|
||||
{ value: 'appearance', label: 'Appearance', Icon: Palette },
|
||||
{ value: 'dictation', label: 'Dictation', Icon: Mic },
|
||||
{ value: 'canvas', label: 'Canvas', Icon: LayoutGrid },
|
||||
{ value: 'agents', label: 'Agents', Icon: Bot },
|
||||
{ value: 'notifications', label: 'Notifications', Icon: Bell },
|
||||
{ value: 'privacy', label: 'Privacy', Icon: ShieldCheck },
|
||||
{ value: 'advanced', label: 'Advanced', Icon: Wrench },
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import ToggleButton from '@mui/material/ToggleButton';
|
||||
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
||||
import { AppSettings } from '@/shared/state/settingsSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import type { SettingsStyles } from '../settingsStyles';
|
||||
import { settingSelectAttrs } from '../settingSelect';
|
||||
import ShortcutRecorderChip from './parts/ShortcutRecorderChip';
|
||||
|
||||
const AgentBehaviorSettings: React.FC<{
|
||||
form: AppSettings;
|
||||
setForm: React.Dispatch<React.SetStateAction<AppSettings>>;
|
||||
styles: SettingsStyles;
|
||||
}> = ({ form, setForm, styles }) => {
|
||||
const c = useClaudeTokens();
|
||||
const { inlineRowSx, inlineRowLastSx, labelSx, descSx, toggleGroupSx } = styles;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={inlineRowSx} {...settingSelectAttrs('new_agent_shortcut', 'New agent shortcut', 'Interface', 'Keyboard shortcut to create an agent.')}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>New agent shortcut</Typography>
|
||||
<Typography sx={descSx}>Keyboard shortcut to create an agent.</Typography>
|
||||
</Box>
|
||||
<ShortcutRecorderChip
|
||||
value={form.new_agent_shortcut}
|
||||
onChange={(combo) => setForm({ ...form, new_agent_shortcut: combo })}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowSx} {...settingSelectAttrs('auto_select_mode_on_new_agent', 'Auto-enable element selection', 'Interface', 'Enter element selection mode when creating a new agent.')}>
|
||||
<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>
|
||||
<ToggleButtonGroup
|
||||
value={form.auto_select_mode_on_new_agent ?? false}
|
||||
exclusive
|
||||
onChange={(_, v) => { if (v !== null) setForm({ ...form, auto_select_mode_on_new_agent: v }); }}
|
||||
size="small"
|
||||
sx={toggleGroupSx}
|
||||
>
|
||||
<ToggleButton value={true}>On</ToggleButton>
|
||||
<ToggleButton value={false}>Off</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowSx} {...settingSelectAttrs('expand_new_chats_in_dashboard', 'Default agent spawn state in dashboard', 'Interface', 'New agents spawn expanded instead of collapsed.')}>
|
||||
<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>
|
||||
{/* Named for the state you get, not on/off: "spawn state" has no obvious on. */}
|
||||
<ToggleButtonGroup
|
||||
value={form.expand_new_chats_in_dashboard ?? false}
|
||||
exclusive
|
||||
onChange={(_, v) => { if (v !== null) setForm({ ...form, expand_new_chats_in_dashboard: v }); }}
|
||||
size="small"
|
||||
sx={toggleGroupSx}
|
||||
>
|
||||
<ToggleButton value={true}>Expanded</ToggleButton>
|
||||
<ToggleButton value={false}>Collapsed</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowLastSx} {...settingSelectAttrs('auto_reveal_sub_agents', 'Auto-reveal sub-agents on dashboard', 'Interface', 'Show sub-agent cards tethered to their parent on the dashboard.')}>
|
||||
<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>
|
||||
<ToggleButtonGroup
|
||||
value={form.auto_reveal_sub_agents ?? false}
|
||||
exclusive
|
||||
onChange={(_, v) => { if (v !== null) setForm({ ...form, auto_reveal_sub_agents: v }); }}
|
||||
size="small"
|
||||
sx={toggleGroupSx}
|
||||
>
|
||||
<ToggleButton value={true}>Show</ToggleButton>
|
||||
<ToggleButton value={false}>Hide</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentBehaviorSettings;
|
||||
@@ -0,0 +1,99 @@
|
||||
import React 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 LanguageIcon from '@mui/icons-material/Language';
|
||||
import { AppSettings } from '@/shared/state/settingsSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import type { SettingsStyles } from '../settingsStyles';
|
||||
import { settingSelectAttrs } from '../settingSelect';
|
||||
|
||||
const CanvasSettings: React.FC<{
|
||||
form: AppSettings;
|
||||
setForm: React.Dispatch<React.SetStateAction<AppSettings>>;
|
||||
styles: SettingsStyles;
|
||||
}> = ({ form, setForm, styles }) => {
|
||||
const c = useClaudeTokens();
|
||||
const { fieldSx, sectionSx, rowSx, rowLastSx, inlineRowSx, labelSx, descSx, toggleGroupSx } = styles;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={inlineRowSx} {...settingSelectAttrs('mouse_wheel_action', 'Mouse wheel', 'Interface', 'What a mouse wheel does on the dashboard canvas.')}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Mouse wheel</Typography>
|
||||
<Typography sx={descSx}>
|
||||
What a plain mouse wheel does on the canvas. The other one moves to cmd/ctrl + wheel.
|
||||
A trackpad two-finger scroll always pans, and pinch always zooms.
|
||||
</Typography>
|
||||
</Box>
|
||||
<ToggleButtonGroup
|
||||
value={form.mouse_wheel_action ?? 'zoom'}
|
||||
exclusive
|
||||
onChange={(_, v) => { if (v !== null) setForm({ ...form, mouse_wheel_action: v }); }}
|
||||
size="small"
|
||||
sx={toggleGroupSx}
|
||||
>
|
||||
<ToggleButton value="zoom">Zoom</ToggleButton>
|
||||
<ToggleButton value="scroll">Scroll</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
|
||||
<Box sx={rowSx} {...settingSelectAttrs('zoom_sensitivity', 'Zoom sensitivity', 'Interface', 'Scroll-to-zoom responsiveness.')}>
|
||||
<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.6875rem' },
|
||||
'& .MuiSlider-valueLabel': { bgcolor: c.accent.primary },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography sx={{ ...sectionSx, mt: 3 }}>Browser</Typography>
|
||||
|
||||
<Box sx={rowLastSx} {...settingSelectAttrs('browser_homepage', 'Default homepage', 'Browser', 'URL loaded when opening a new browser card.')}>
|
||||
<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 CanvasSettings;
|
||||
@@ -0,0 +1,136 @@
|
||||
import React 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 { AppSettings } from '@/shared/state/settingsSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import type { SettingsStyles } from '../settingsStyles';
|
||||
import { settingSelectAttrs } from '../settingSelect';
|
||||
import ShortcutRecorderChip, { dictationDefaultCombo, comboDisplay } from './parts/ShortcutRecorderChip';
|
||||
import DictationModelPicker from './parts/DictationModelPicker';
|
||||
import DictationHistoryList from './parts/DictationHistoryList';
|
||||
|
||||
const DictationSettings: React.FC<{
|
||||
form: AppSettings;
|
||||
setForm: React.Dispatch<React.SetStateAction<AppSettings>>;
|
||||
styles: SettingsStyles;
|
||||
}> = ({ form, setForm, styles }) => {
|
||||
const c = useClaudeTokens();
|
||||
const { inlineRowSx, labelSx, descSx, toggleGroupSx } = styles;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<Box sx={inlineRowSx} {...settingSelectAttrs('voice_hold_to_talk', 'Dictation', 'Interface', 'Hold to talk, or tap to start and stop.')}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Dictation</Typography>
|
||||
<Typography sx={descSx}>{`How the mic button and the dictation shortcut (${comboDisplay(form.dictation_shortcut || dictationDefaultCombo())}) work.`}</Typography>
|
||||
</Box>
|
||||
<ToggleButtonGroup
|
||||
value={form.voice_hold_to_talk ?? true}
|
||||
exclusive
|
||||
onChange={(_, v) => {
|
||||
if (v === null) return;
|
||||
setForm({ ...form, voice_hold_to_talk: v });
|
||||
// Keyboard hold needs the native key tap; picking Hold on a Mac without the Accessibility
|
||||
// grant fires the system prompt so the choice can actually take effect after a relaunch.
|
||||
if (v) void window.openswarm?.voiceRequestHoldPermission?.();
|
||||
}}
|
||||
size="small"
|
||||
sx={toggleGroupSx}
|
||||
>
|
||||
<ToggleButton value={true}>Hold to talk</ToggleButton>
|
||||
<ToggleButton value={false}>Tap to toggle</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowSx} {...settingSelectAttrs('dictation_shortcut', 'Dictation shortcut', 'Interface', 'Global hotkey that starts dictation.')}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Dictation shortcut</Typography>
|
||||
<Typography sx={descSx}>Works anywhere, even with the app in the background.</Typography>
|
||||
</Box>
|
||||
<ShortcutRecorderChip
|
||||
value={form.dictation_shortcut || dictationDefaultCombo()}
|
||||
onChange={(combo) => setForm({ ...form, dictation_shortcut: combo })}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowSx} {...settingSelectAttrs('dictation_model', 'Dictation model', 'Interface', 'Speech model used for dictation; bigger is more accurate but slower.')}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Dictation model</Typography>
|
||||
<Typography sx={descSx}>Runs on this machine, nothing is uploaded. Bigger is more accurate but slower to transcribe.</Typography>
|
||||
</Box>
|
||||
<DictationModelPicker
|
||||
value={form.dictation_model ?? null}
|
||||
onChange={(id) => setForm({ ...form, dictation_model: id })}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowSx} {...settingSelectAttrs('dictation_dictionary', 'Dictation dictionary', 'Interface', 'Names and jargon dictation should always spell right.')}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Dictionary</Typography>
|
||||
<Typography sx={descSx}>Comma-separated names and jargon (people, products, acronyms) that dictation should always spell right.</Typography>
|
||||
</Box>
|
||||
<TextField
|
||||
size="small"
|
||||
placeholder="Anthropic, Kubernetes, OpenSwarm"
|
||||
value={form.dictation_dictionary ?? ''}
|
||||
onChange={(e) => setForm({ ...form, dictation_dictionary: e.target.value })}
|
||||
sx={{ width: 280 }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowSx} {...settingSelectAttrs('dictation_sounds', 'Dictation sounds', 'Interface', 'The start, stop, and text-landed cues.')}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Sounds</Typography>
|
||||
<Typography sx={descSx}>The start, stop, and text-landed cues, and how loud they play.</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<Slider
|
||||
size="small"
|
||||
min={0}
|
||||
max={100}
|
||||
disabled={!(form.dictation_sounds ?? true)}
|
||||
value={Math.round((form.dictation_sound_volume ?? 0.35) * 100)}
|
||||
onChange={(_, v) => setForm({ ...form, dictation_sound_volume: (v as number) / 100 })}
|
||||
sx={{ width: 110 }}
|
||||
/>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={form.dictation_sounds ?? true}
|
||||
onChange={(e) => setForm({ ...form, dictation_sounds: e.target.checked })}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowSx} {...settingSelectAttrs('dictation_disabled_surfaces', 'Dictation off for sites', 'Interface', 'Sites where the dictation key does nothing.')}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Off for sites</Typography>
|
||||
<Typography sx={descSx}>Comma-separated hostnames where the dictation key refuses to record (it tells you instead of failing silently).</Typography>
|
||||
</Box>
|
||||
<TextField
|
||||
size="small"
|
||||
placeholder="docs.google.com, slack.com"
|
||||
value={form.dictation_disabled_surfaces ?? ''}
|
||||
onChange={(e) => setForm({ ...form, dictation_disabled_surfaces: e.target.value })}
|
||||
sx={{ width: 280 }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ ...inlineRowSx, alignItems: 'flex-start' }} {...settingSelectAttrs('dictation_history', 'Dictation history', 'Interface', 'Your recent dictations, copyable.')}>
|
||||
<Box sx={{ mr: 3, flexShrink: 0, width: 220 }}>
|
||||
<Typography sx={labelSx}>History</Typography>
|
||||
<Typography sx={descSx}>Recent dictations, stored only on this machine. Copy one back if it landed in the wrong place.</Typography>
|
||||
</Box>
|
||||
<DictationHistoryList />
|
||||
</Box>
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DictationSettings;
|
||||
@@ -95,252 +95,6 @@ const GeneralInterface: React.FC<{
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
||||
<Typography sx={sectionSx}>Dictation</Typography>
|
||||
|
||||
<Box sx={inlineRowSx} {...settingSelectAttrs('voice_hold_to_talk', 'Dictation', 'Interface', 'Hold to talk, or tap to start and stop.')}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Dictation</Typography>
|
||||
<Typography sx={descSx}>{`How the mic button and the dictation shortcut (${comboDisplay(form.dictation_shortcut || dictationDefaultCombo())}) work.`}</Typography>
|
||||
</Box>
|
||||
<ToggleButtonGroup
|
||||
value={form.voice_hold_to_talk ?? true}
|
||||
exclusive
|
||||
onChange={(_, v) => {
|
||||
if (v === null) return;
|
||||
setForm({ ...form, voice_hold_to_talk: v });
|
||||
// Keyboard hold needs the native key tap; picking Hold on a Mac without the Accessibility
|
||||
// grant fires the system prompt so the choice can actually take effect after a relaunch.
|
||||
if (v) void window.openswarm?.voiceRequestHoldPermission?.();
|
||||
}}
|
||||
size="small"
|
||||
sx={toggleGroupSx}
|
||||
>
|
||||
<ToggleButton value={true}>Hold to talk</ToggleButton>
|
||||
<ToggleButton value={false}>Tap to toggle</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowSx} {...settingSelectAttrs('dictation_shortcut', 'Dictation shortcut', 'Interface', 'Global hotkey that starts dictation.')}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Dictation shortcut</Typography>
|
||||
<Typography sx={descSx}>Works anywhere, even with the app in the background.</Typography>
|
||||
</Box>
|
||||
<ShortcutRecorderChip
|
||||
value={form.dictation_shortcut || dictationDefaultCombo()}
|
||||
onChange={(combo) => setForm({ ...form, dictation_shortcut: combo })}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowSx} {...settingSelectAttrs('dictation_model', 'Dictation model', 'Interface', 'Speech model used for dictation; bigger is more accurate but slower.')}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Dictation model</Typography>
|
||||
<Typography sx={descSx}>Runs on this machine, nothing is uploaded. Bigger is more accurate but slower to transcribe.</Typography>
|
||||
</Box>
|
||||
<DictationModelPicker
|
||||
value={form.dictation_model ?? null}
|
||||
onChange={(id) => setForm({ ...form, dictation_model: id })}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowSx} {...settingSelectAttrs('dictation_dictionary', 'Dictation dictionary', 'Interface', 'Names and jargon dictation should always spell right.')}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Dictionary</Typography>
|
||||
<Typography sx={descSx}>Comma-separated names and jargon (people, products, acronyms) that dictation should always spell right.</Typography>
|
||||
</Box>
|
||||
<TextField
|
||||
size="small"
|
||||
placeholder="Anthropic, Kubernetes, OpenSwarm"
|
||||
value={form.dictation_dictionary ?? ''}
|
||||
onChange={(e) => setForm({ ...form, dictation_dictionary: e.target.value })}
|
||||
sx={{ width: 280 }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowSx} {...settingSelectAttrs('dictation_sounds', 'Dictation sounds', 'Interface', 'The start, stop, and text-landed cues.')}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Sounds</Typography>
|
||||
<Typography sx={descSx}>The start, stop, and text-landed cues, and how loud they play.</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<Slider
|
||||
size="small"
|
||||
min={0}
|
||||
max={100}
|
||||
disabled={!(form.dictation_sounds ?? true)}
|
||||
value={Math.round((form.dictation_sound_volume ?? 0.35) * 100)}
|
||||
onChange={(_, v) => setForm({ ...form, dictation_sound_volume: (v as number) / 100 })}
|
||||
sx={{ width: 110 }}
|
||||
/>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={form.dictation_sounds ?? true}
|
||||
onChange={(e) => setForm({ ...form, dictation_sounds: e.target.checked })}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowSx} {...settingSelectAttrs('dictation_disabled_surfaces', 'Dictation off for sites', 'Interface', 'Sites where the dictation key does nothing.')}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Off for sites</Typography>
|
||||
<Typography sx={descSx}>Comma-separated hostnames where the dictation key refuses to record (it tells you instead of failing silently).</Typography>
|
||||
</Box>
|
||||
<TextField
|
||||
size="small"
|
||||
placeholder="docs.google.com, slack.com"
|
||||
value={form.dictation_disabled_surfaces ?? ''}
|
||||
onChange={(e) => setForm({ ...form, dictation_disabled_surfaces: e.target.value })}
|
||||
sx={{ width: 280 }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ ...inlineRowSx, alignItems: 'flex-start' }} {...settingSelectAttrs('dictation_history', 'Dictation history', 'Interface', 'Your recent dictations, copyable.')}>
|
||||
<Box sx={{ mr: 3, flexShrink: 0, width: 220 }}>
|
||||
<Typography sx={labelSx}>History</Typography>
|
||||
<Typography sx={descSx}>Recent dictations, stored only on this machine. Copy one back if it landed in the wrong place.</Typography>
|
||||
</Box>
|
||||
<DictationHistoryList />
|
||||
</Box>
|
||||
|
||||
<Typography sx={sectionSx}>Canvas</Typography>
|
||||
|
||||
<Box sx={inlineRowSx} {...settingSelectAttrs('mouse_wheel_action', 'Mouse wheel', 'Interface', 'What a mouse wheel does on the dashboard canvas.')}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Mouse wheel</Typography>
|
||||
<Typography sx={descSx}>
|
||||
What a plain mouse wheel does on the canvas. The other one moves to cmd/ctrl + wheel.
|
||||
A trackpad two-finger scroll always pans, and pinch always zooms.
|
||||
</Typography>
|
||||
</Box>
|
||||
<ToggleButtonGroup
|
||||
value={form.mouse_wheel_action ?? 'zoom'}
|
||||
exclusive
|
||||
onChange={(_, v) => { if (v !== null) setForm({ ...form, mouse_wheel_action: v }); }}
|
||||
size="small"
|
||||
sx={toggleGroupSx}
|
||||
>
|
||||
<ToggleButton value="zoom">Zoom</ToggleButton>
|
||||
<ToggleButton value="scroll">Scroll</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
|
||||
<Box sx={rowSx} {...settingSelectAttrs('zoom_sensitivity', 'Zoom sensitivity', 'Interface', 'Scroll-to-zoom responsiveness.')}>
|
||||
<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.6875rem' },
|
||||
'& .MuiSlider-valueLabel': { bgcolor: c.accent.primary },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Typography sx={sectionSx}>Agents</Typography>
|
||||
|
||||
<Box sx={inlineRowSx} {...settingSelectAttrs('new_agent_shortcut', 'New agent shortcut', 'Interface', 'Keyboard shortcut to create an agent.')}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>New agent shortcut</Typography>
|
||||
<Typography sx={descSx}>Keyboard shortcut to create an agent.</Typography>
|
||||
</Box>
|
||||
<ShortcutRecorderChip
|
||||
value={form.new_agent_shortcut}
|
||||
onChange={(combo) => setForm({ ...form, new_agent_shortcut: combo })}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowSx} {...settingSelectAttrs('auto_select_mode_on_new_agent', 'Auto-enable element selection', 'Interface', 'Enter element selection mode when creating a new agent.')}>
|
||||
<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>
|
||||
<ToggleButtonGroup
|
||||
value={form.auto_select_mode_on_new_agent ?? false}
|
||||
exclusive
|
||||
onChange={(_, v) => { if (v !== null) setForm({ ...form, auto_select_mode_on_new_agent: v }); }}
|
||||
size="small"
|
||||
sx={toggleGroupSx}
|
||||
>
|
||||
<ToggleButton value={true}>On</ToggleButton>
|
||||
<ToggleButton value={false}>Off</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowSx} {...settingSelectAttrs('expand_new_chats_in_dashboard', 'Default agent spawn state in dashboard', 'Interface', 'New agents spawn expanded instead of collapsed.')}>
|
||||
<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>
|
||||
{/* Named for the state you get, not on/off: "spawn state" has no obvious on. */}
|
||||
<ToggleButtonGroup
|
||||
value={form.expand_new_chats_in_dashboard ?? false}
|
||||
exclusive
|
||||
onChange={(_, v) => { if (v !== null) setForm({ ...form, expand_new_chats_in_dashboard: v }); }}
|
||||
size="small"
|
||||
sx={toggleGroupSx}
|
||||
>
|
||||
<ToggleButton value={true}>Expanded</ToggleButton>
|
||||
<ToggleButton value={false}>Collapsed</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowLastSx} {...settingSelectAttrs('auto_reveal_sub_agents', 'Auto-reveal sub-agents on dashboard', 'Interface', 'Show sub-agent cards tethered to their parent on the dashboard.')}>
|
||||
<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>
|
||||
<ToggleButtonGroup
|
||||
value={form.auto_reveal_sub_agents ?? false}
|
||||
exclusive
|
||||
onChange={(_, v) => { if (v !== null) setForm({ ...form, auto_reveal_sub_agents: v }); }}
|
||||
size="small"
|
||||
sx={toggleGroupSx}
|
||||
>
|
||||
<ToggleButton value={true}>Show</ToggleButton>
|
||||
<ToggleButton value={false}>Hide</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
|
||||
<Typography sx={{ ...sectionSx, mt: 3 }}>Browser</Typography>
|
||||
|
||||
<Box sx={rowLastSx} {...settingSelectAttrs('browser_homepage', 'Default homepage', 'Browser', 'URL loaded when opening a new browser card.')}>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user