diff --git a/frontend/src/app/pages/Settings/SettingsBody.tsx b/frontend/src/app/pages/Settings/SettingsBody.tsx index f57d3d26..8fd7cc6d 100644 --- a/frontend/src/app/pages/Settings/SettingsBody.tsx +++ b/frontend/src/app/pages/Settings/SettingsBody.tsx @@ -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 = ({ active, onRequestClose }) = + ) : activeTab === 'dictation' ? ( + + + + ) : activeTab === 'canvas' ? ( + + + + ) : activeTab === 'agents' ? ( + + + ) : activeTab === 'privacy' ? ( diff --git a/frontend/src/app/pages/Settings/sections/SettingsRail.tsx b/frontend/src/app/pages/Settings/sections/SettingsRail.tsx index 5198157b..b489543f 100644 --- a/frontend/src/app/pages/Settings/sections/SettingsRail.tsx +++ b/frontend/src/app/pages/Settings/sections/SettingsRail.tsx @@ -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 }, diff --git a/frontend/src/app/pages/Settings/sections/general/AgentBehaviorSettings.tsx b/frontend/src/app/pages/Settings/sections/general/AgentBehaviorSettings.tsx new file mode 100644 index 00000000..40fc8659 --- /dev/null +++ b/frontend/src/app/pages/Settings/sections/general/AgentBehaviorSettings.tsx @@ -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>; + styles: SettingsStyles; +}> = ({ form, setForm, styles }) => { + const c = useClaudeTokens(); + const { inlineRowSx, inlineRowLastSx, labelSx, descSx, toggleGroupSx } = styles; + + return ( + <> + + + New agent shortcut + Keyboard shortcut to create an agent. + + setForm({ ...form, new_agent_shortcut: combo })} + /> + + + + + Auto-enable element selection + Automatically enter element selection mode when creating a new agent. + + { if (v !== null) setForm({ ...form, auto_select_mode_on_new_agent: v }); }} + size="small" + sx={toggleGroupSx} + > + On + Off + + + + + + Default agent spawn state in dashboard + When enabled, new agents spawn expanded instead of collapsed. + + {/* Named for the state you get, not on/off: "spawn state" has no obvious on. */} + { if (v !== null) setForm({ ...form, expand_new_chats_in_dashboard: v }); }} + size="small" + sx={toggleGroupSx} + > + Expanded + Collapsed + + + + + + Auto-reveal sub-agents on dashboard + Automatically show sub-agent cards (from CreateAgent / InvokeAgent) tethered to their parent on the dashboard. + + { if (v !== null) setForm({ ...form, auto_reveal_sub_agents: v }); }} + size="small" + sx={toggleGroupSx} + > + Show + Hide + + + + ); +}; + +export default AgentBehaviorSettings; diff --git a/frontend/src/app/pages/Settings/sections/general/CanvasSettings.tsx b/frontend/src/app/pages/Settings/sections/general/CanvasSettings.tsx new file mode 100644 index 00000000..3f02baec --- /dev/null +++ b/frontend/src/app/pages/Settings/sections/general/CanvasSettings.tsx @@ -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>; + styles: SettingsStyles; +}> = ({ form, setForm, styles }) => { + const c = useClaudeTokens(); + const { fieldSx, sectionSx, rowSx, rowLastSx, inlineRowSx, labelSx, descSx, toggleGroupSx } = styles; + + return ( + <> + + + Mouse wheel + + 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. + + + { if (v !== null) setForm({ ...form, mouse_wheel_action: v }); }} + size="small" + sx={toggleGroupSx} + > + Zoom + Scroll + + + + + Zoom sensitivity + + Scroll-to-zoom responsiveness. Lower for trackpads, higher for mouse wheels. + + + 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 }, + }} + /> + + + Browser + + + Default homepage + + URL loaded when opening a new browser card on the dashboard. + + + + 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, + }, + }} + /> + + + + ); +}; + +export default CanvasSettings; diff --git a/frontend/src/app/pages/Settings/sections/general/DictationSettings.tsx b/frontend/src/app/pages/Settings/sections/general/DictationSettings.tsx new file mode 100644 index 00000000..b8470dfe --- /dev/null +++ b/frontend/src/app/pages/Settings/sections/general/DictationSettings.tsx @@ -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>; + styles: SettingsStyles; +}> = ({ form, setForm, styles }) => { + const c = useClaudeTokens(); + const { inlineRowSx, labelSx, descSx, toggleGroupSx } = styles; + + return ( + <> + + + + Dictation + {`How the mic button and the dictation shortcut (${comboDisplay(form.dictation_shortcut || dictationDefaultCombo())}) work.`} + + { + 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} + > + Hold to talk + Tap to toggle + + + + + + Dictation shortcut + Works anywhere, even with the app in the background. + + setForm({ ...form, dictation_shortcut: combo })} + /> + + + + + Dictation model + Runs on this machine, nothing is uploaded. Bigger is more accurate but slower to transcribe. + + setForm({ ...form, dictation_model: id })} + /> + + + + + Dictionary + Comma-separated names and jargon (people, products, acronyms) that dictation should always spell right. + + setForm({ ...form, dictation_dictionary: e.target.value })} + sx={{ width: 280 }} + /> + + + + + Sounds + The start, stop, and text-landed cues, and how loud they play. + + + setForm({ ...form, dictation_sound_volume: (v as number) / 100 })} + sx={{ width: 110 }} + /> + setForm({ ...form, dictation_sounds: e.target.checked })} + /> + + + + + + Off for sites + Comma-separated hostnames where the dictation key refuses to record (it tells you instead of failing silently). + + setForm({ ...form, dictation_disabled_surfaces: e.target.value })} + sx={{ width: 280 }} + /> + + + + + History + Recent dictations, stored only on this machine. Copy one back if it landed in the wrong place. + + + + + + ); +}; + +export default DictationSettings; diff --git a/frontend/src/app/pages/Settings/sections/general/GeneralInterface.tsx b/frontend/src/app/pages/Settings/sections/general/GeneralInterface.tsx index 32d2862c..5dae9d6f 100644 --- a/frontend/src/app/pages/Settings/sections/general/GeneralInterface.tsx +++ b/frontend/src/app/pages/Settings/sections/general/GeneralInterface.tsx @@ -95,252 +95,6 @@ const GeneralInterface: React.FC<{ /> - - Dictation - - - - Dictation - {`How the mic button and the dictation shortcut (${comboDisplay(form.dictation_shortcut || dictationDefaultCombo())}) work.`} - - { - 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} - > - Hold to talk - Tap to toggle - - - - - - Dictation shortcut - Works anywhere, even with the app in the background. - - setForm({ ...form, dictation_shortcut: combo })} - /> - - - - - Dictation model - Runs on this machine, nothing is uploaded. Bigger is more accurate but slower to transcribe. - - setForm({ ...form, dictation_model: id })} - /> - - - - - Dictionary - Comma-separated names and jargon (people, products, acronyms) that dictation should always spell right. - - setForm({ ...form, dictation_dictionary: e.target.value })} - sx={{ width: 280 }} - /> - - - - - Sounds - The start, stop, and text-landed cues, and how loud they play. - - - setForm({ ...form, dictation_sound_volume: (v as number) / 100 })} - sx={{ width: 110 }} - /> - setForm({ ...form, dictation_sounds: e.target.checked })} - /> - - - - - - Off for sites - Comma-separated hostnames where the dictation key refuses to record (it tells you instead of failing silently). - - setForm({ ...form, dictation_disabled_surfaces: e.target.value })} - sx={{ width: 280 }} - /> - - - - - History - Recent dictations, stored only on this machine. Copy one back if it landed in the wrong place. - - - - - Canvas - - - - Mouse wheel - - 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. - - - { if (v !== null) setForm({ ...form, mouse_wheel_action: v }); }} - size="small" - sx={toggleGroupSx} - > - Zoom - Scroll - - - - - Zoom sensitivity - - Scroll-to-zoom responsiveness. Lower for trackpads, higher for mouse wheels. - - - 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 }, - }} - /> - - - - Agents - - - - New agent shortcut - Keyboard shortcut to create an agent. - - setForm({ ...form, new_agent_shortcut: combo })} - /> - - - - - Auto-enable element selection - Automatically enter element selection mode when creating a new agent. - - { if (v !== null) setForm({ ...form, auto_select_mode_on_new_agent: v }); }} - size="small" - sx={toggleGroupSx} - > - On - Off - - - - - - Default agent spawn state in dashboard - When enabled, new agents spawn expanded instead of collapsed. - - {/* Named for the state you get, not on/off: "spawn state" has no obvious on. */} - { if (v !== null) setForm({ ...form, expand_new_chats_in_dashboard: v }); }} - size="small" - sx={toggleGroupSx} - > - Expanded - Collapsed - - - - - - Auto-reveal sub-agents on dashboard - Automatically show sub-agent cards (from CreateAgent / InvokeAgent) tethered to their parent on the dashboard. - - { if (v !== null) setForm({ ...form, auto_reveal_sub_agents: v }); }} - size="small" - sx={toggleGroupSx} - > - Show - Hide - - - - Browser - - - Default homepage - - URL loaded when opening a new browser card on the dashboard. - - - - 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, - }, - }} - /> - - ); };