From 4a38fd6ebc4f031acb0f7d79690209f4669e6383 Mon Sep 17 00:00:00 2001 From: Phantom Date: Mon, 1 Dec 2025 17:04:43 +0800 Subject: [PATCH] feat(ui): new Switch (#11061) * refactor(ui): migrate switch component from heroui to radix-ui replace heroui switch implementation with radix-ui for better maintainability update package.json and yarn.lock to include new dependency * fix(eslint): enable heroui import restriction for deprecated Switch component * refactor(ui): update Switch component props from isSelected/onValueChange to checked/onCheckedChange Standardize Switch component props across the codebase to use checked/onCheckedChange instead of isSelected/onValueChange for better consistency with common React patterns. Also updates loading state prop from isLoading to loading and removes size prop where unnecessary. The changes include: - Replacing isSelected with checked - Replacing onValueChange with onCheckedChange - Updating isLoading to loading - Removing redundant size props - Adjusting styling to accommodate new loading state * refactor(switch): improve switch component styling and structure - Add default values for loading and disabled props - Update styling classes and add group cursor pointer - Restructure loading indicator and thumb positioning - Wrap DescriptionSwitch children in flex container * refactor(ui): improve switch component structure and usage - Restructure DescriptionSwitch to use explicit props instead of children - Add label, description, and position props for better customization - Update all switch usages in SettingsTab to use new props format * refactor(primitives): simplify switch props by omitting children Remove redundant children prop from CustomSwitchProps since it's already omitted from the parent type * fix(switch): add useId for label accessibility in DescriptionSwitch Ensure proper label association with switch input by generating unique ID using React's useId hook * refactor(settings): remove commented out SettingRowTitleSmall components * refactor(SettingsTab): add todo comment for memoization optimization * feat(switch): add size prop to customize switch dimensions Add sm, md, and lg size options to the Switch component with corresponding styles. This allows for better visual consistency across different UI contexts. * style(ui): adjust switch component styling and theme colors update switch component layout and spacing to improve consistency modify secondary-foreground color variable to use correct semantic token * feat(switch): add new switch component styles and animations - Add new switch.css file with gradient and transition styles - Update switch.tsx component with new styling classes and animations - Remove loader icon in favor of animated gradient effect * fix(i18n): Auto update translations for PR #11061 * style(primitives): remove redundant border style from switch component * refactor(switch): remove switch.css and update switch component styles Remove deprecated switch.css file and migrate styles to inline tailwind classes. Update disabled state styling to use opacity instead of linear gradient for better consistency. * refactor(switch): simplify switch thumb implementation Replace complex div structure with svg for loading state Adjust disabled opacity and loading state styling * style(switch): adjust thumb size and positioning for better consistency * feat(switch): add storybook documentation for switch component Add comprehensive Storybook documentation for the Switch component, including: - Basic usage examples - Different states (checked, disabled, loading) - Size variations - DescriptionSwitch variant - Real-world usage scenarios - Accessibility examples - Form integration examples Also remove redundant box-content class from switch styles * fix(switch): adjust thumb positioning for md and lg sizes * style(primitives): improve switch component styling and spacing - Add padding to the container - Simplify label height logic - Update typography classes for better consistency - Adjust switch container alignment * feat(switch): add size prop to DescriptionSwitch component Add support for sm, md, and lg sizes to DescriptionSwitch component with responsive text sizing. Also includes comprehensive Storybook documentation with examples of all sizes and states. * style(switch): align label text to right when isLeftSide is true * refactor(stories): clean up DescriptionSwitch stories by removing unused imports and simplifying JSX * refactor(ui): rename CustomizedSwitch to Switch for consistency Simplify component naming by removing redundant 'Customized' prefix and aligning with common naming conventions * refactor(switch): extract switch root styles into cva variants Improve maintainability by using class-variance-authority to manage switch root styles and variants * refactor(switch): extract thumb variants into separate cva function Improve maintainability by moving switch thumb styling logic into a dedicated variants configuration. This makes the component more readable and easier to modify. * feat(switch): add classNames prop for custom styling Allow custom class names to be applied to switch root, thumb, and thumbSvg elements for more flexible styling options. * feat(switch): add loading animation variants for switch thumb Extract loading animation logic into separate cva variants for better maintainability and reusability --------- Co-authored-by: GitHub Action --- eslint.config.mjs | 38 +- package.json | 1 + .../ui/src/components/primitives/switch.tsx | 212 ++++- .../primitives/DescriptionSwitch.stories.tsx | 823 ++++++++++++++++++ .../components/primitives/Switch.stories.tsx | 666 ++++++++++++++ .../src/components/ObsidianExportDialog.tsx | 2 +- .../src/pages/home/Tabs/SettingsTab.tsx | 280 +++--- .../MiniappSettings/MiniAppSettings.tsx | 6 +- .../src/pages/paintings/AihubmixPage.tsx | 4 +- .../src/pages/paintings/DmxapiPage.tsx | 2 +- .../src/pages/paintings/SiliconPage.tsx | 4 +- .../components/DynamicFormRender.tsx | 4 +- .../src/pages/settings/AboutSettings.tsx | 4 +- .../AgentSettings/ToolingSettings.tsx | 11 +- .../AssistantMCPSettings.tsx | 5 +- .../AssistantMemorySettings.tsx | 4 +- .../AssistantModelSettings.tsx | 16 +- .../settings/DataSettings/DataSettings.tsx | 8 +- .../DataSettings/ExportMenuSettings.tsx | 40 +- .../settings/DataSettings/JoplinSettings.tsx | 2 +- .../DataSettings/LocalBackupSettings.tsx | 2 +- .../DataSettings/MarkdownExportSettings.tsx | 12 +- .../settings/DataSettings/NotionSettings.tsx | 2 +- .../DataSettings/NutstoreSettings.tsx | 2 +- .../settings/DataSettings/S3Settings.tsx | 2 +- .../settings/DataSettings/WebDavSettings.tsx | 4 +- .../DisplaySettings/DisplaySettings.tsx | 10 +- .../src/pages/settings/GeneralSettings.tsx | 30 +- .../settings/MCPSettings/McpServerCard.tsx | 5 +- .../settings/MCPSettings/McpSettings.tsx | 8 +- .../pages/settings/MCPSettings/McpTool.tsx | 7 +- .../MemorySettings/MemorySettings.tsx | 2 +- .../DefaultAssistantSettings.tsx | 12 +- .../ModelSettings/QuickModelPopup.tsx | 2 +- .../src/pages/settings/NotesSettings.tsx | 8 +- .../ApiOptionsSettings/ApiOptionsSettings.tsx | 2 +- .../EditModelPopup/ModelEditContent.tsx | 5 +- .../ProviderSettings/ProviderSetting.tsx | 4 +- .../pages/settings/QuickAssistantSettings.tsx | 6 +- .../SelectionAssistantSettings.tsx | 14 +- .../src/pages/settings/ShortcutSettings.tsx | 2 +- .../WebSearchSettings/BasicSettings.tsx | 2 +- .../src/pages/translate/TranslateSettings.tsx | 16 +- yarn.lock | 55 +- 44 files changed, 1993 insertions(+), 353 deletions(-) create mode 100644 packages/ui/stories/components/primitives/DescriptionSwitch.stories.tsx create mode 100644 packages/ui/stories/components/primitives/Switch.stories.tsx diff --git a/eslint.config.mjs b/eslint.config.mjs index ef610da5fb..2ff7fd9f41 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -143,19 +143,31 @@ export default defineConfig([ files: ['**/*.{ts,tsx,js,jsx}'], ignores: [], rules: { - // 'no-restricted-imports': [ - // 'error', - // { - // paths: [ - // { - // name: 'antd', - // importNames: ['Flex', 'Switch', 'message', 'Button', 'Tooltip'], - // message: - // '❌ Do not import this component from antd. Use our custom components instead: import { ... } from "@cherrystudio/ui"' - // } - // ] - // } - // ] + 'no-restricted-imports': [ + 'error', + { + paths: [ + // { + // name: 'antd', + // importNames: ['Flex', 'Switch', 'message', 'Button', 'Tooltip'], + // message: + // '❌ Do not import this component from antd. Use our custom components instead: import { ... } from "@cherrystudio/ui"' + // }, + { + name: 'antd', + importNames: ['Switch'], + message: + '❌ Do not import this component from antd. Use our custom components instead: import { ... } from "@cherrystudio/ui"' + }, + { + name: '@heroui/react', + importNames: ['Switch'], + message: + '❌ Do not import the component from heroui directly. It\'s deprecated.' + } + ] + } + ] } }, // Schema key naming convention (cache & preferences) diff --git a/package.json b/package.json index 794e0ab309..4e1ac0061b 100644 --- a/package.json +++ b/package.json @@ -179,6 +179,7 @@ "@opeoginni/github-copilot-openai-compatible": "^0.1.21", "@playwright/test": "^1.55.1", "@radix-ui/react-context-menu": "^2.2.16", + "@radix-ui/react-switch": "^1.2.6", "@reduxjs/toolkit": "^2.2.5", "@shikijs/markdown-it": "^3.12.0", "@swc/plugin-styled-components": "^8.0.4", diff --git a/packages/ui/src/components/primitives/switch.tsx b/packages/ui/src/components/primitives/switch.tsx index 51d9721603..7ce2ac5d3c 100644 --- a/packages/ui/src/components/primitives/switch.tsx +++ b/packages/ui/src/components/primitives/switch.tsx @@ -1,54 +1,178 @@ -import type { SwitchProps } from '@heroui/react' -import { cn, Spinner, Switch } from '@heroui/react' +import { cn } from '@cherrystudio/ui/utils' +import * as SwitchPrimitive from '@radix-ui/react-switch' +import { cva } from 'class-variance-authority' +import * as React from 'react' +import { useId } from 'react' + +const switchRootVariants = cva( + [ + 'cs-switch cs-switch-root', + 'group relative cursor-pointer peer inline-flex shrink-0 items-center rounded-full shadow-xs outline-none transition-all', + 'data-[state=unchecked]:bg-gray-500/20 data-[state=checked]:bg-primary', + 'disabled:cursor-not-allowed disabled:opacity-40', + 'focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50' + ], + { + variants: { + size: { + sm: ['w-9 h-5'], + md: ['w-11 h-5.5'], + lg: ['w-11 h-6'] + }, + loading: { + false: null, + true: ['bg-primary-hover!'] + } + }, + defaultVariants: { + size: 'md', + loading: false + } + } +) + +const switchThumbVariants = cva( + [ + 'cs-switch cs-switch-thumb', + 'pointer-events-none block rounded-full ring-0 transition-all data-[state=unchecked]:translate-x-0' + ], + { + variants: { + size: { + sm: ['size-4.5 ml-[1px] data-[state=checked]:translate-x-4'], + md: ['size-[19px] ml-0.5 data-[state=checked]:translate-x-[21px]'], + lg: ['size-5 ml-[3px] data-[state=checked]:translate-x-4.5'] + }, + loading: { + false: null, + true: ['bg-primary-hover!'] + } + }, + compoundVariants: [ + { + size: 'sm', + loading: true, + className: 'size-3.5 ml-0.5 data-[state=checked]:translate-x-4.5' + }, + { + size: 'md', + loading: true, + className: 'size-4 ml-1 data-[state=checked]:translate-x-5' + }, + { + size: 'lg', + loading: true, + className: 'size-4.5 ml-1 data-[state=checked]:translate-x-4.5' + } + ] + } +) + +const switchThumbSvgVariants = cva(['transition-all'], { + variants: { + loading: { + false: null, + true: ['animate-spin'] + } + }, + defaultVariants: { + loading: false + } +}) // Enhanced Switch component with loading state support -interface CustomSwitchProps extends SwitchProps { - isLoading?: boolean +interface SwitchProps extends Omit, 'children'> { + /** When true, displays a loading animation in the switch thumb. Defaults to false when undefined. */ + loading?: boolean + size?: 'sm' | 'md' | 'lg' + classNames?: { + root?: string + thumb?: string + thumbSvg?: string + } } -/** - * A customized Switch component based on HeroUI Switch - * @see https://www.heroui.com/docs/components/switch#api - * @param isLoading When true, displays a loading spinner in the switch thumb - */ -const CustomizedSwitch = ({ isLoading, children, ref, thumbIcon, ...props }: CustomSwitchProps) => { - const finalThumbIcon = isLoading ? : thumbIcon - +function Switch({ loading = false, size = 'md', className, classNames, ...props }: SwitchProps) { return ( - - {children} - - ) -} - -const DescriptionSwitch = ({ children, ...props }: CustomSwitchProps) => { - return ( - - {children} - + + + + + + ) } -CustomizedSwitch.displayName = 'Switch' +interface DescriptionSwitchProps extends SwitchProps { + /** Text label displayed next to the switch. */ + label: string + /** Optional helper text shown below the label. */ + description?: string + /** Switch position relative to label. Defaults to 'right'. */ + position?: 'left' | 'right' +} -export { DescriptionSwitch, CustomizedSwitch as Switch } -export type { CustomSwitchProps as SwitchProps } +// TODO: It's not finished. We need to use Typography components instead of native html element. +const DescriptionSwitch = ({ + label, + description, + position = 'right', + size = 'md', + ...props +}: DescriptionSwitchProps) => { + const isLeftSide = position === 'left' + const id = useId() + return ( +
+ +
+ +
+
+ ) +} + +Switch.displayName = 'Switch' + +export { DescriptionSwitch, Switch } +export type { SwitchProps } diff --git a/packages/ui/stories/components/primitives/DescriptionSwitch.stories.tsx b/packages/ui/stories/components/primitives/DescriptionSwitch.stories.tsx new file mode 100644 index 0000000000..9587870e27 --- /dev/null +++ b/packages/ui/stories/components/primitives/DescriptionSwitch.stories.tsx @@ -0,0 +1,823 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { Bell, Eye, Lock, Moon, Shield, Wifi, Zap } from 'lucide-react' +import { useState } from 'react' + +import { DescriptionSwitch } from '../../../src/components/primitives/switch' + +const meta: Meta = { + title: 'Components/Primitives/DescriptionSwitch', + component: DescriptionSwitch, + parameters: { + layout: 'centered', + docs: { + description: { + component: + 'An enhanced Switch component with integrated label and optional description text. Perfect for settings panels and preference forms where context is important. Built on top of the Radix UI Switch primitive with support for multiple sizes, loading states, and flexible positioning.' + } + } + }, + tags: ['autodocs'], + argTypes: { + label: { + control: { type: 'text' }, + description: 'Text label displayed next to the switch (required)' + }, + description: { + control: { type: 'text' }, + description: 'Optional helper text shown below the label' + }, + position: { + control: { type: 'select' }, + options: ['left', 'right'], + description: 'Switch position relative to label' + }, + disabled: { + control: { type: 'boolean' }, + description: 'Whether the switch is disabled' + }, + loading: { + control: { type: 'boolean' }, + description: 'When true, displays a loading animation in the switch thumb' + }, + size: { + control: { type: 'select' }, + options: ['sm', 'md', 'lg'], + description: 'The size of the switch' + }, + defaultChecked: { + control: { type: 'boolean' }, + description: 'Default checked state' + }, + checked: { + control: { type: 'boolean' }, + description: 'Checked state in controlled mode' + } + } +} + +export default meta +type Story = StoryObj + +// Default +export const Default: Story = { + render: () => ( +
+ +
+ ) +} + +// Without Description +export const WithoutDescription: Story = { + render: () => ( +
+ + + +
+ ) +} + +// With Description +export const WithDescription: Story = { + render: () => ( +
+ + + +
+ ) +} + +// Positions +export const Positions: Story = { + render: () => ( +
+
+

Switch on Right (Default)

+
+ + + +
+
+ +
+

Switch on Left

+
+ + + +
+
+
+ ) +} + +// Sizes +export const Sizes: Story = { + render: () => ( +
+
+

Small (sm)

+ +
+ +
+

Medium (md) - Default

+ +
+ +
+

Large (lg)

+ +
+
+ ) +} + +// States +export const States: Story = { + render: () => ( +
+
+

Normal (Unchecked)

+ +
+ +
+

Checked

+ +
+ +
+

Disabled (Unchecked)

+ +
+ +
+

Disabled (Checked)

+ +
+ +
+

Loading

+ +
+
+ ) +} + +// Controlled +export const Controlled: Story = { + render: function ControlledExample() { + const [checked, setChecked] = useState(false) + + return ( +
+
+ +
+
+
Current state: {checked ? 'On' : 'Off'}
+ +
+
+ ) + } +} + +// Long Text +export const LongText: Story = { + render: () => ( +
+ + +
+ ) +} + +// Notification Settings Example +export const NotificationSettings: Story = { + render: function NotificationSettingsExample() { + const [notifications, setNotifications] = useState({ + email: true, + push: false, + sms: false, + desktop: true, + mobile: false, + weekly: true + }) + + return ( +
+
+

Notification Preferences

+
+ setNotifications({ ...notifications, email: !!checked })} + /> + setNotifications({ ...notifications, push: !!checked })} + /> + setNotifications({ ...notifications, sms: !!checked })} + /> + setNotifications({ ...notifications, desktop: !!checked })} + /> + setNotifications({ ...notifications, mobile: !!checked })} + /> + setNotifications({ ...notifications, weekly: !!checked })} + /> +
+
+
+ ) + } +} + +// Privacy Settings Example +export const PrivacySettings: Story = { + render: function PrivacySettingsExample() { + const [privacy, setPrivacy] = useState({ + profileVisible: true, + activityTracking: false, + dataSharing: false, + personalization: true, + thirdParty: false + }) + + return ( +
+
+

Privacy & Data

+
+ setPrivacy({ ...privacy, profileVisible: !!checked })} + /> + setPrivacy({ ...privacy, activityTracking: !!checked })} + /> + setPrivacy({ ...privacy, dataSharing: !!checked })} + /> + setPrivacy({ ...privacy, personalization: !!checked })} + /> + setPrivacy({ ...privacy, thirdParty: !!checked })} + /> +
+
+
+ ) + } +} + +// Application Settings Example +export const ApplicationSettings: Story = { + render: function ApplicationSettingsExample() { + const [settings, setSettings] = useState({ + autoSave: true, + spellCheck: true, + darkMode: false, + compactMode: false, + animations: true, + sound: false, + offlineMode: false + }) + + return ( +
+
+

Application Settings

+
+ setSettings({ ...settings, autoSave: !!checked })} + /> + setSettings({ ...settings, spellCheck: !!checked })} + /> + setSettings({ ...settings, darkMode: !!checked })} + /> + setSettings({ ...settings, compactMode: !!checked })} + /> + setSettings({ ...settings, animations: !!checked })} + /> + setSettings({ ...settings, sound: !!checked })} + /> + setSettings({ ...settings, offlineMode: !!checked })} + /> +
+
+
+ ) + } +} + +// With Icons +export const WithIcons: Story = { + render: () => ( +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ ) +} + +// Loading Simulation +export const LoadingSimulation: Story = { + render: function LoadingSimulationExample() { + const [states, setStates] = useState({ + wifi: { enabled: false, loading: false }, + bluetooth: { enabled: false, loading: false }, + location: { enabled: false, loading: false } + }) + + const handleToggle = async (setting: keyof typeof states, checked: boolean) => { + setStates((prev) => ({ + ...prev, + [setting]: { ...prev[setting], loading: true } + })) + + // Simulate API call + await new Promise((resolve) => setTimeout(resolve, 1500)) + + setStates((prev) => ({ + ...prev, + [setting]: { enabled: checked, loading: false } + })) + } + + return ( +
+
+

System Settings

+
+ handleToggle('wifi', !!checked)} + loading={states.wifi.loading} + disabled={states.wifi.loading} + /> + handleToggle('bluetooth', !!checked)} + loading={states.bluetooth.loading} + disabled={states.bluetooth.loading} + /> + handleToggle('location', !!checked)} + loading={states.location.loading} + disabled={states.location.loading} + /> +
+
+

Toggle switches to see a simulated 1.5-second loading state

+
+ ) + } +} + +// Complex Settings Panel +export const ComplexSettingsPanel: Story = { + render: function ComplexSettingsPanelExample() { + const [settings, setSettings] = useState({ + notifications: { + email: true, + push: false, + desktop: true + }, + privacy: { + profile: true, + activity: false, + analytics: true + }, + features: { + autoSave: true, + darkMode: false, + compactView: false + }, + security: { + twoFactor: false, + biometric: true, + sessionTimeout: false + } + }) + + return ( +
+ {/* Notifications Section */} +
+
+ +

Notifications

+
+
+ + setSettings({ + ...settings, + notifications: { ...settings.notifications, email: !!checked } + }) + } + /> + + setSettings({ + ...settings, + notifications: { ...settings.notifications, push: !!checked } + }) + } + /> + + setSettings({ + ...settings, + notifications: { ...settings.notifications, desktop: !!checked } + }) + } + /> +
+
+ + {/* Privacy Section */} +
+
+ +

Privacy

+
+
+ + setSettings({ + ...settings, + privacy: { ...settings.privacy, profile: !!checked } + }) + } + /> + + setSettings({ + ...settings, + privacy: { ...settings.privacy, activity: !!checked } + }) + } + /> + + setSettings({ + ...settings, + privacy: { ...settings.privacy, analytics: !!checked } + }) + } + /> +
+
+ + {/* Features Section */} +
+
+ +

Features

+
+
+ + setSettings({ + ...settings, + features: { ...settings.features, autoSave: !!checked } + }) + } + /> + + setSettings({ + ...settings, + features: { ...settings.features, darkMode: !!checked } + }) + } + /> + + setSettings({ + ...settings, + features: { ...settings.features, compactView: !!checked } + }) + } + /> +
+
+ + {/* Security Section */} +
+
+ +

Security

+
+
+ + setSettings({ + ...settings, + security: { ...settings.security, twoFactor: !!checked } + }) + } + /> + + setSettings({ + ...settings, + security: { ...settings.security, biometric: !!checked } + }) + } + /> + + setSettings({ + ...settings, + security: { ...settings.security, sessionTimeout: !!checked } + }) + } + /> +
+
+
+ ) + } +} + +// Accessibility Features +export const AccessibilityFeatures: Story = { + render: () => ( +
+
+

Keyboard Navigation

+

+ Use Tab to navigate between switches and Space/Enter to toggle them. Each switch has a proper label for screen + readers. +

+
+ + + + +
+
+
+ ) +} + +// Responsive Layout +export const ResponsiveLayout: Story = { + render: () => ( +
+
+

Narrow Layout (300px)

+
+ + +
+
+ +
+

Standard Layout (500px)

+
+ + +
+
+ +
+

Wide Layout (700px)

+
+ + +
+
+
+ ) +} diff --git a/packages/ui/stories/components/primitives/Switch.stories.tsx b/packages/ui/stories/components/primitives/Switch.stories.tsx new file mode 100644 index 0000000000..958701496c --- /dev/null +++ b/packages/ui/stories/components/primitives/Switch.stories.tsx @@ -0,0 +1,666 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { Bell, Moon, Shield, Wifi, Zap } from 'lucide-react' +import { useState } from 'react' + +import { DescriptionSwitch, Switch } from '../../../src/components/primitives/switch' + +const meta: Meta = { + title: 'Components/Primitives/Switch', + component: Switch, + parameters: { + layout: 'centered', + docs: { + description: { + component: + 'A switch component based on Radix UI Switch, allowing users to toggle between on/off states. Supports three sizes (sm, md, lg), loading states, and an enhanced DescriptionSwitch variant with label and description. Built with accessibility in mind.' + } + } + }, + tags: ['autodocs'], + argTypes: { + disabled: { + control: { type: 'boolean' }, + description: 'Whether the switch is disabled' + }, + loading: { + control: { type: 'boolean' }, + description: 'When true, displays a loading animation in the switch thumb' + }, + size: { + control: { type: 'select' }, + options: ['sm', 'md', 'lg'], + description: 'The size of the switch' + }, + defaultChecked: { + control: { type: 'boolean' }, + description: 'Default checked state' + }, + checked: { + control: { type: 'boolean' }, + description: 'Checked state in controlled mode' + } + } +} + +export default meta +type Story = StoryObj + +// Default +export const Default: Story = { + render: () => ( +
+
+ + +
+
+ + +
+
+ + +
+
+ ) +} + +// With Default Checked +export const WithDefaultChecked: Story = { + render: () => ( +
+
+ + +
+
+ + +
+
+ + +
+
+ ) +} + +// Disabled +export const Disabled: Story = { + render: () => ( +
+
+ + +
+
+ + +
+
+ ) +} + +// Loading State +export const Loading: Story = { + render: () => ( +
+
+ + +
+
+ + +
+
+ + +
+
+ ) +} + +// Controlled +export const Controlled: Story = { + render: function ControlledExample() { + const [checked, setChecked] = useState(false) + + return ( +
+
+ + +
+
Current state: {checked ? 'On' : 'Off'}
+ +
+ ) + } +} + +// Sizes +export const Sizes: Story = { + render: () => ( +
+
+

Small (sm)

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

Medium (md) - Default

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

Large (lg)

+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ ) +} + +// Description Switch - Basic +export const DescriptionSwitchBasic: Story = { + render: () => ( +
+ + + +
+ ) +} + +// Description Switch - Positions +export const DescriptionSwitchPositions: Story = { + render: () => ( +
+
+

Switch on Right (Default)

+
+ + +
+
+ +
+

Switch on Left

+
+ + +
+
+
+ ) +} + +// Description Switch - Sizes +export const DescriptionSwitchSizes: Story = { + render: () => ( +
+ + + +
+ ) +} + +// Description Switch - States +export const DescriptionSwitchStates: Story = { + render: () => ( +
+ + + + + +
+ ) +} + +// Size Comparison +export const SizeComparison: Story = { + render: () => ( +
+
+

Off

+
+
+ + sm +
+
+ + md +
+
+ + lg +
+
+
+ +
+

On

+
+
+ + sm +
+
+ + md +
+
+ + lg +
+
+
+ +
+

Loading

+
+
+ + sm +
+
+ + md +
+
+ + lg +
+
+
+
+ ) +} + +// Real World Examples +export const RealWorldExamples: Story = { + render: function RealWorldExample() { + const [settings, setSettings] = useState({ + notifications: true, + autoSave: true, + darkMode: false, + analytics: true + }) + + const [privacy, setPrivacy] = useState({ + shareData: false, + allowCookies: true, + trackLocation: false, + personalizedAds: false + }) + + return ( +
+ {/* General Settings */} +
+

General Settings

+
+
+ setSettings({ ...settings, notifications: !!checked })} + /> + +
+
+ setSettings({ ...settings, autoSave: !!checked })} + /> + +
+
+ setSettings({ ...settings, darkMode: !!checked })} + /> + +
+
+ setSettings({ ...settings, analytics: !!checked })} + /> + +
+
+
+ + {/* Privacy Settings with DescriptionSwitch */} +
+

Privacy Settings

+
+ setPrivacy({ ...privacy, shareData: !!checked })} + /> + setPrivacy({ ...privacy, allowCookies: !!checked })} + /> + setPrivacy({ ...privacy, trackLocation: !!checked })} + /> + setPrivacy({ ...privacy, personalizedAds: !!checked })} + /> +
+
+
+ ) + } +} + +// Interactive Loading Example +export const InteractiveLoading: Story = { + render: function InteractiveLoadingExample() { + const [isLoading, setIsLoading] = useState(false) + const [isEnabled, setIsEnabled] = useState(false) + + const handleToggle = async (checked: boolean) => { + setIsLoading(true) + // Simulate API call + await new Promise((resolve) => setTimeout(resolve, 2000)) + setIsEnabled(checked) + setIsLoading(false) + } + + return ( +
+
+ +
+
+ + Status: + + {isLoading ? 'Connecting...' : isEnabled ? 'Connected' : 'Disconnected'} + +
+
+
+

Click the switch to see a simulated 2-second loading state

+
+ ) + } +} + +// Form Example +export const FormExample: Story = { + render: function FormExample() { + const [formData, setFormData] = useState({ + emailNotifications: true, + pushNotifications: false, + smsNotifications: false, + newsletter: true, + twoFactorAuth: false, + biometricAuth: true + }) + + const [isSaving, setIsSaving] = useState(false) + const [saved, setSaved] = useState(false) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setIsSaving(true) + setSaved(false) + // Simulate API call + await new Promise((resolve) => setTimeout(resolve, 1500)) + setIsSaving(false) + setSaved(true) + setTimeout(() => setSaved(false), 3000) + } + + return ( +
+

Account Preferences

+ +
+
+

Notifications

+
+ setFormData({ ...formData, emailNotifications: !!checked })} + disabled={isSaving} + /> + setFormData({ ...formData, pushNotifications: !!checked })} + disabled={isSaving} + /> + setFormData({ ...formData, smsNotifications: !!checked })} + disabled={isSaving} + /> + setFormData({ ...formData, newsletter: !!checked })} + disabled={isSaving} + /> +
+
+ +
+

Security

+
+ setFormData({ ...formData, twoFactorAuth: !!checked })} + disabled={isSaving} + /> + setFormData({ ...formData, biometricAuth: !!checked })} + disabled={isSaving} + /> +
+
+
+ +
+ + {saved &&

Settings saved successfully!

} +
+
+ ) + } +} + +// Accessibility Example +export const Accessibility: Story = { + render: () => ( +
+
+

Keyboard Navigation

+

+ Use Tab to navigate between switches and Space/Enter to toggle them. +

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

ARIA Labels

+

+ Switches include proper ARIA attributes for screen reader support. +

+ +
+
+ ) +} diff --git a/src/renderer/src/components/ObsidianExportDialog.tsx b/src/renderer/src/components/ObsidianExportDialog.tsx index 8d4a2fab3d..1a2ce51174 100644 --- a/src/renderer/src/components/ObsidianExportDialog.tsx +++ b/src/renderer/src/components/ObsidianExportDialog.tsx @@ -415,7 +415,7 @@ const PopupContainer: React.FC = ({ {!rawContent && ( - + )} diff --git a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx index 68a14cca21..b5d3709ec4 100644 --- a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx @@ -115,6 +115,7 @@ const SettingsTab: FC = (props) => { const { theme } = useTheme() const { themeNames } = useCodeStyle() + // FIXME: We should use useMemo to calculate these states instead of using useEffect to sync const [temperature, setTemperature] = useState(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE) const [enableTemperature, setEnableTemperature] = useState(assistant?.settings?.enableTemperature ?? true) const [contextCount, setContextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT) @@ -270,10 +271,9 @@ const SettingsTab: FC = (props) => { { + checked={enableTemperature} + onCheckedChange={(enabled) => { setEnableTemperature(enabled) onUpdateAssistantSettings({ enableTemperature: enabled }) }} @@ -340,9 +340,8 @@ const SettingsTab: FC = (props) => { {t('models.stream_output')} { + checked={streamOutput} + onCheckedChange={(checked) => { setStreamOutput(checked) onUpdateAssistantSettings({ streamOutput: checked }) }} @@ -357,9 +356,8 @@ const SettingsTab: FC = (props) => { { + checked={enableMaxTokens} + onCheckedChange={async (enabled) => { if (enabled) { const confirmed = await modalConfirm({ title: t('chat.settings.max_tokens.confirm'), @@ -410,38 +408,36 @@ const SettingsTab: FC = (props) => { - - {t('settings.messages.prompt')} - - - - - {/* {t('settings.messages.use_serif_font')} */} setMessageFont(checked ? 'serif' : 'system')}> - {t('settings.messages.use_serif_font')} - + checked={showPrompt} + onCheckedChange={setShowPrompt} + label={t('settings.messages.prompt')} + /> - {/* - {t('chat.settings.thought_auto_collapse.label')} - - */} - - - {t('chat.settings.thought_auto_collapse.label')} - - - + setMessageFont(checked ? 'serif' : 'system')} + label={t('settings.messages.use_serif_font')} + /> - - {t('settings.messages.show_message_outline')} - + + + + + @@ -534,16 +530,12 @@ const SettingsTab: FC = (props) => { - {/* - {t('settings.math.single_dollar.label')} - - */} - - - {t('settings.math.single_dollar.label')} - - - + @@ -567,32 +559,21 @@ const SettingsTab: FC = (props) => { - {/* - {t('chat.settings.code_fancy_block.label')} - - */} - - - {t('chat.settings.code_fancy_block.label')} - - - + - {/* - {t('chat.settings.code_execution.title')} - - */} setCodeExecution({ enabled: checked })}> - - {t('chat.settings.code_execution.title')} - - - + checked={codeExecution.enabled} + onCheckedChange={(checked) => setCodeExecution({ enabled: checked })} + label={t('chat.settings.code_execution.title')} + description={t('chat.settings.code_execution.tip')} + /> {codeExecution.enabled && ( <> @@ -616,90 +597,80 @@ const SettingsTab: FC = (props) => { )} - {/* {t('chat.settings.code_editor.title')} */} setCodeEditor({ enabled: checked })}> - {t('chat.settings.code_editor.title')} - + checked={codeEditor.enabled} + onCheckedChange={(checked) => setCodeEditor({ enabled: checked })} + label={t('chat.settings.code_editor.title')} + /> {codeEditor.enabled && ( <> - {/* - {t('chat.settings.code_editor.highlight_active_line')} - - */} setCodeEditor({ highlightActiveLine: checked })}> - - {t('chat.settings.code_editor.highlight_active_line')} - - - + checked={codeEditor.highlightActiveLine} + onCheckedChange={(checked) => setCodeEditor({ highlightActiveLine: checked })} + label={t('chat.settings.code_editor.highlight_active_line')} + /> - {/* {t('chat.settings.code_editor.fold_gutter')} */} setCodeEditor({ foldGutter: checked })}> - {t('chat.settings.code_editor.fold_gutter')} - + checked={codeEditor.foldGutter} + onCheckedChange={(checked) => setCodeEditor({ foldGutter: checked })} + label={t('chat.settings.code_editor.fold_gutter')} + /> - {/* {t('chat.settings.code_editor.autocompletion')} */} setCodeEditor({ autocompletion: checked })}> - {t('chat.settings.code_editor.autocompletion')} - + checked={codeEditor.autocompletion} + onCheckedChange={(checked) => setCodeEditor({ autocompletion: checked })} + label={t('chat.settings.code_editor.autocompletion')} + /> - {/* {t('chat.settings.code_editor.keymap')} */} setCodeEditor({ keymap: checked })}> - {t('chat.settings.code_editor.keymap')} - + checked={codeEditor.keymap} + onCheckedChange={(checked) => setCodeEditor({ keymap: checked })} + label={t('chat.settings.code_editor.keymap')} + /> )} - - {t('chat.settings.show_line_numbers')} - + - - {t('chat.settings.code_collapsible')} - + - - {t('chat.settings.code_wrappable')} - + - - - {t('chat.settings.code_image_tools.label')} - - - + @@ -708,17 +679,18 @@ const SettingsTab: FC = (props) => { - {t('settings.messages.input.show_estimated_tokens')} - + checked={showInputEstimatedTokens} + onCheckedChange={setShowInputEstimatedTokens} + label={t('settings.messages.input.show_estimated_tokens')} + /> - - {t('settings.messages.input.paste_long_text_as_file')} - + {pasteLongTextAsFile && ( <> @@ -740,54 +712,54 @@ const SettingsTab: FC = (props) => { - {t('settings.messages.markdown_rendering_input_message')} - + checked={renderInputMessageAsMarkdown} + onCheckedChange={setRenderInputMessageAsMarkdown} + label={t('settings.messages.markdown_rendering_input_message')} + /> {!(language || navigator.language).startsWith('en') && ( <> - {t('settings.input.auto_translate_with_space')} - + checked={autoTranslateWithSpace} + onCheckedChange={setAutoTranslateWithSpace} + label={t('settings.input.auto_translate_with_space')} + /> )} - - {t('settings.input.show_translate_confirm')} - + - {t('settings.messages.input.enable_quick_triggers')} - - - - - - {t('settings.messages.input.confirm_delete_message')} - + checked={enableQuickPanelTriggers} + onCheckedChange={setEnableQuickPanelTriggers} + label={t('settings.messages.input.enable_quick_triggers')} + /> - {t('settings.messages.input.confirm_regenerate_message')} - + checked={confirmDeleteMessage} + onCheckedChange={setConfirmDeleteMessage} + label={t('settings.messages.input.confirm_delete_message')} + /> + + + + diff --git a/src/renderer/src/pages/minapps/MiniappSettings/MiniAppSettings.tsx b/src/renderer/src/pages/minapps/MiniappSettings/MiniAppSettings.tsx index d71df5cd93..19c2324983 100644 --- a/src/renderer/src/pages/minapps/MiniappSettings/MiniAppSettings.tsx +++ b/src/renderer/src/pages/minapps/MiniappSettings/MiniAppSettings.tsx @@ -96,7 +96,7 @@ const MiniAppSettings: FC = () => { {t('settings.miniapps.open_link_external.title')} - setMinappsOpenLinkExternal(checked)} /> + setMinappsOpenLinkExternal(checked)} /> {/* 缓存小程序数量设置 */} @@ -134,8 +134,8 @@ const MiniAppSettings: FC = () => { {t('settings.miniapps.sidebar_description')} setShowOpenedMinappsInSidebar(checked)} + checked={showOpenedMinappsInSidebar} + onCheckedChange={(checked) => setShowOpenedMinappsInSidebar(checked)} /> diff --git a/src/renderer/src/pages/paintings/AihubmixPage.tsx b/src/renderer/src/pages/paintings/AihubmixPage.tsx index cbc6881558..294bed47f2 100644 --- a/src/renderer/src/pages/paintings/AihubmixPage.tsx +++ b/src/renderer/src/pages/paintings/AihubmixPage.tsx @@ -800,8 +800,8 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { return ( updatePaintingState({ [item.key!]: checked })} + checked={(painting[item.key!] || item.initialValue) as boolean} + onCheckedChange={(checked) => updatePaintingState({ [item.key!]: checked })} /> ) diff --git a/src/renderer/src/pages/paintings/DmxapiPage.tsx b/src/renderer/src/pages/paintings/DmxapiPage.tsx index 09cdf1dc2d..7f83183d28 100644 --- a/src/renderer/src/pages/paintings/DmxapiPage.tsx +++ b/src/renderer/src/pages/paintings/DmxapiPage.tsx @@ -938,7 +938,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { - onChangeAutoCreate(checked)} /> + onChangeAutoCreate(checked)} /> diff --git a/src/renderer/src/pages/paintings/SiliconPage.tsx b/src/renderer/src/pages/paintings/SiliconPage.tsx index 0fc025c22f..16ff35363a 100644 --- a/src/renderer/src/pages/paintings/SiliconPage.tsx +++ b/src/renderer/src/pages/paintings/SiliconPage.tsx @@ -464,8 +464,8 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => { updatePaintingState({ promptEnhancement: checked })} + checked={painting.promptEnhancement} + onCheckedChange={(checked) => updatePaintingState({ promptEnhancement: checked })} /> diff --git a/src/renderer/src/pages/paintings/components/DynamicFormRender.tsx b/src/renderer/src/pages/paintings/components/DynamicFormRender.tsx index ce1dbbe2d1..cf8735c3bd 100644 --- a/src/renderer/src/pages/paintings/components/DynamicFormRender.tsx +++ b/src/renderer/src/pages/paintings/components/DynamicFormRender.tsx @@ -198,8 +198,8 @@ export const DynamicFormRender: React.FC = ({ if (type === 'boolean') { return ( onChange(propertyName, checked)} + checked={value !== undefined ? value : defaultValue} + onCheckedChange={(checked) => onChange(propertyName, checked)} style={{ width: '2px' }} /> ) diff --git a/src/renderer/src/pages/settings/AboutSettings.tsx b/src/renderer/src/pages/settings/AboutSettings.tsx index f55b56775d..571914ac4e 100644 --- a/src/renderer/src/pages/settings/AboutSettings.tsx +++ b/src/renderer/src/pages/settings/AboutSettings.tsx @@ -228,13 +228,13 @@ const AboutSettings: FC = () => { {t('settings.general.auto_check_update.title')} - setAutoCheckUpdate(v)} /> + setAutoCheckUpdate(v)} /> {t('settings.general.test_plan.title')} - handleSetTestPlan(v)} /> + handleSetTestPlan(v)} /> {testPlan && ( diff --git a/src/renderer/src/pages/settings/AgentSettings/ToolingSettings.tsx b/src/renderer/src/pages/settings/AgentSettings/ToolingSettings.tsx index 40dc2249a6..22126c3685 100644 --- a/src/renderer/src/pages/settings/AgentSettings/ToolingSettings.tsx +++ b/src/renderer/src/pages/settings/AgentSettings/ToolingSettings.tsx @@ -1,3 +1,4 @@ +import { Switch } from '@cherrystudio/ui' import { permissionModeCards } from '@renderer/config/agent' import { useMCPServers } from '@renderer/hooks/useMCPServers' import useScrollPosition from '@renderer/hooks/useScrollPosition' @@ -13,7 +14,7 @@ import type { } from '@renderer/types' import { AgentConfigurationSchema } from '@renderer/types' import { Modal, Tag } from 'antd' -import { Alert, Card, Input, Switch } from 'antd' +import { Alert, Card, Input } from 'antd' import { ShieldAlert, Wrench } from 'lucide-react' import type { FC } from 'react' import { useCallback, useMemo, useState } from 'react' @@ -401,8 +402,8 @@ export const ToolingSettings: FC = ({ agentBase, upda })} checked={isApproved} disabled={isAuto || isUpdatingTools} - size="small" - onChange={(checked) => handleToggleTool(tool.id, checked)} + size="sm" + onCheckedChange={(checked) => handleToggleTool(tool.id, checked)} /> } @@ -483,9 +484,9 @@ export const ToolingSettings: FC = ({ agentBase, upda name: server.name })} checked={isSelected} - size="small" + size="sm" disabled={!server.isActive || isUpdatingMcp} - onChange={(checked) => handleToggleMcp(server.id, checked)} + onCheckedChange={(checked) => handleToggleMcp(server.id, checked)} /> } diff --git a/src/renderer/src/pages/settings/AssistantSettings/AssistantMCPSettings.tsx b/src/renderer/src/pages/settings/AssistantSettings/AssistantMCPSettings.tsx index d9bb273d8f..64f66e9cdf 100644 --- a/src/renderer/src/pages/settings/AssistantSettings/AssistantMCPSettings.tsx +++ b/src/renderer/src/pages/settings/AssistantSettings/AssistantMCPSettings.tsx @@ -87,10 +87,9 @@ const AssistantMCPSettings: React.FC = ({ assistant, updateAssistant }) = : undefined }> handleServerToggle(server.id)} - size="sm" + onCheckedChange={() => handleServerToggle(server.id)} /> diff --git a/src/renderer/src/pages/settings/AssistantSettings/AssistantMemorySettings.tsx b/src/renderer/src/pages/settings/AssistantSettings/AssistantMemorySettings.tsx index 60cc2b5b85..b323b6dfcd 100644 --- a/src/renderer/src/pages/settings/AssistantSettings/AssistantMemorySettings.tsx +++ b/src/renderer/src/pages/settings/AssistantSettings/AssistantMemorySettings.tsx @@ -94,8 +94,8 @@ const AssistantMemorySettings: React.FC = ({ assistant, updateAssistant, : '' }> diff --git a/src/renderer/src/pages/settings/AssistantSettings/AssistantModelSettings.tsx b/src/renderer/src/pages/settings/AssistantSettings/AssistantModelSettings.tsx index 120b10103c..1a981251f3 100644 --- a/src/renderer/src/pages/settings/AssistantSettings/AssistantModelSettings.tsx +++ b/src/renderer/src/pages/settings/AssistantSettings/AssistantModelSettings.tsx @@ -246,8 +246,8 @@ const AssistantModelSettings: FC = ({ assistant, updateAssistant, updateA { + checked={enableTemperature} + onCheckedChange={(enabled) => { setEnableTemperature(enabled) updateAssistantSettings({ enableTemperature: enabled }) }} @@ -295,8 +295,8 @@ const AssistantModelSettings: FC = ({ assistant, updateAssistant, updateA /> { + checked={enableTopP} + onCheckedChange={(enabled) => { setEnableTopP(enabled) updateAssistantSettings({ enableTopP: enabled }) }} @@ -387,8 +387,8 @@ const AssistantModelSettings: FC = ({ assistant, updateAssistant, updateA /> { + checked={enableMaxTokens} + onCheckedChange={async (enabled) => { if (enabled) { const confirmed = await modalConfirm({ title: t('chat.settings.max_tokens.confirm'), @@ -430,8 +430,8 @@ const AssistantModelSettings: FC = ({ assistant, updateAssistant, updateA { + checked={streamOutput} + onCheckedChange={(checked) => { setStreamOutput(checked) updateAssistantSettings({ streamOutput: checked }) }} diff --git a/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx b/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx index 0e9bba3ef2..5b98bda4fb 100644 --- a/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx @@ -6,7 +6,7 @@ import { WifiOutlined, YuqueOutlined } from '@ant-design/icons' -import { Button, RowFlex } from '@cherrystudio/ui' +import { Button, RowFlex, Switch } from '@cherrystudio/ui' import { usePreference } from '@data/hooks/usePreference' import DividerWithText from '@renderer/components/DividerWithText' import { NutstoreIcon } from '@renderer/components/Icons/NutstoreIcons' @@ -22,7 +22,7 @@ import { reset } from '@renderer/services/BackupService' import type { AppInfo } from '@renderer/types' import { formatFileSize } from '@renderer/utils' import { occupiedDirs } from '@shared/config/constant' -import { Progress, Switch, Typography } from 'antd' +import { Progress, Typography } from 'antd' import { FileText, FolderCog, FolderInput, FolderOpen, SaveIcon } from 'lucide-react' import type { FC } from 'react' import { useEffect, useState } from 'react' @@ -291,7 +291,7 @@ const DataSettings: FC = () => { (shouldCopyData = checked)} + onCheckedChange={(checked) => (shouldCopyData = checked)} style={{ marginRight: '8px' }} title={t('settings.data.app_data.copy_data_option')} /> @@ -616,7 +616,7 @@ const DataSettings: FC = () => { {t('settings.data.backup.skip_file_data_title')} - + {t('settings.data.backup.skip_file_data_help')} diff --git a/src/renderer/src/pages/settings/DataSettings/ExportMenuSettings.tsx b/src/renderer/src/pages/settings/DataSettings/ExportMenuSettings.tsx index 974f760239..878560d477 100644 --- a/src/renderer/src/pages/settings/DataSettings/ExportMenuSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/ExportMenuSettings.tsx @@ -35,18 +35,15 @@ const ExportMenuOptions: FC = () => { {t('settings.data.export_menu.image')} - handleToggleOption('image', checked)} - /> + handleToggleOption('image', checked)} /> {t('settings.data.export_menu.markdown')} handleToggleOption('markdown', checked)} + checked={exportMenuOptions.markdown} + onCheckedChange={(checked) => handleToggleOption('markdown', checked)} /> @@ -54,8 +51,8 @@ const ExportMenuOptions: FC = () => { {t('settings.data.export_menu.markdown_reason')} handleToggleOption('markdown_reason', checked)} + checked={exportMenuOptions.markdown_reason} + onCheckedChange={(checked) => handleToggleOption('markdown_reason', checked)} /> @@ -63,26 +60,23 @@ const ExportMenuOptions: FC = () => { {t('settings.data.export_menu.notion')} handleToggleOption('notion', checked)} + checked={exportMenuOptions.notion} + onCheckedChange={(checked) => handleToggleOption('notion', checked)} /> {t('settings.data.export_menu.yuque')} - handleToggleOption('yuque', checked)} - /> + handleToggleOption('yuque', checked)} /> {t('settings.data.export_menu.joplin')} handleToggleOption('joplin', checked)} + checked={exportMenuOptions.joplin} + onCheckedChange={(checked) => handleToggleOption('joplin', checked)} /> @@ -90,8 +84,8 @@ const ExportMenuOptions: FC = () => { {t('settings.data.export_menu.obsidian')} handleToggleOption('obsidian', checked)} + checked={exportMenuOptions.obsidian} + onCheckedChange={(checked) => handleToggleOption('obsidian', checked)} /> @@ -99,23 +93,23 @@ const ExportMenuOptions: FC = () => { {t('settings.data.export_menu.siyuan')} handleToggleOption('siyuan', checked)} + checked={exportMenuOptions.siyuan} + onCheckedChange={(checked) => handleToggleOption('siyuan', checked)} /> {t('settings.data.export_menu.docx')} - handleToggleOption('docx', checked)} /> + handleToggleOption('docx', checked)} /> {t('settings.data.export_menu.plain_text')} handleToggleOption('plain_text', checked)} + checked={exportMenuOptions.plain_text} + onCheckedChange={(checked) => handleToggleOption('plain_text', checked)} /> diff --git a/src/renderer/src/pages/settings/DataSettings/JoplinSettings.tsx b/src/renderer/src/pages/settings/DataSettings/JoplinSettings.tsx index b5b72af37c..d34affe07a 100644 --- a/src/renderer/src/pages/settings/DataSettings/JoplinSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/JoplinSettings.tsx @@ -122,7 +122,7 @@ const JoplinSettings: FC = () => { {t('settings.data.joplin.export_reasoning.title')} - + {t('settings.data.joplin.export_reasoning.help')} diff --git a/src/renderer/src/pages/settings/DataSettings/LocalBackupSettings.tsx b/src/renderer/src/pages/settings/DataSettings/LocalBackupSettings.tsx index 5278540af8..f70c3270a5 100644 --- a/src/renderer/src/pages/settings/DataSettings/LocalBackupSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/LocalBackupSettings.tsx @@ -261,7 +261,7 @@ const LocalBackupSettings: React.FC = () => { {t('settings.data.backup.skip_file_data_title')} - + {t('settings.data.backup.skip_file_data_help')} diff --git a/src/renderer/src/pages/settings/DataSettings/MarkdownExportSettings.tsx b/src/renderer/src/pages/settings/DataSettings/MarkdownExportSettings.tsx index 7384660314..c9c8497c74 100644 --- a/src/renderer/src/pages/settings/DataSettings/MarkdownExportSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/MarkdownExportSettings.tsx @@ -98,7 +98,7 @@ const MarkdownExportSettings: FC = () => { {t('settings.data.markdown_export.force_dollar_math.title')} - + {t('settings.data.markdown_export.force_dollar_math.help')} @@ -106,7 +106,7 @@ const MarkdownExportSettings: FC = () => { {t('settings.data.message_title.use_topic_naming.title')} - + {t('settings.data.message_title.use_topic_naming.help')} @@ -114,7 +114,7 @@ const MarkdownExportSettings: FC = () => { {t('settings.data.markdown_export.show_model_name.title')} - + {t('settings.data.markdown_export.show_model_name.help')} @@ -122,7 +122,7 @@ const MarkdownExportSettings: FC = () => { {t('settings.data.markdown_export.show_model_provider.title')} - + {t('settings.data.markdown_export.show_model_provider.help')} @@ -130,7 +130,7 @@ const MarkdownExportSettings: FC = () => { {t('settings.data.markdown_export.exclude_citations.title')} - + {t('settings.data.markdown_export.exclude_citations.help')} @@ -138,7 +138,7 @@ const MarkdownExportSettings: FC = () => { {t('settings.data.markdown_export.standardize_citations.title')} - + {t('settings.data.markdown_export.standardize_citations.help')} diff --git a/src/renderer/src/pages/settings/DataSettings/NotionSettings.tsx b/src/renderer/src/pages/settings/DataSettings/NotionSettings.tsx index 25e0dd1b1e..eb9d116db9 100644 --- a/src/renderer/src/pages/settings/DataSettings/NotionSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/NotionSettings.tsx @@ -128,7 +128,7 @@ const NotionSettings: FC = () => { {t('settings.data.notion.export_reasoning.title')} - + {t('settings.data.notion.export_reasoning.help')} diff --git a/src/renderer/src/pages/settings/DataSettings/NutstoreSettings.tsx b/src/renderer/src/pages/settings/DataSettings/NutstoreSettings.tsx index 4a6ab7c2d1..ec7f81345d 100644 --- a/src/renderer/src/pages/settings/DataSettings/NutstoreSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/NutstoreSettings.tsx @@ -319,7 +319,7 @@ const NutstoreSettings: FC = () => { {t('settings.data.backup.skip_file_data_title')} - + {t('settings.data.backup.skip_file_data_help')} diff --git a/src/renderer/src/pages/settings/DataSettings/S3Settings.tsx b/src/renderer/src/pages/settings/DataSettings/S3Settings.tsx index 2da603ad64..2f1fe25073 100644 --- a/src/renderer/src/pages/settings/DataSettings/S3Settings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/S3Settings.tsx @@ -243,7 +243,7 @@ const S3Settings: FC = () => { {t('settings.data.s3.skipBackupFile.label')} - + {t('settings.data.s3.skipBackupFile.help')} diff --git a/src/renderer/src/pages/settings/DataSettings/WebDavSettings.tsx b/src/renderer/src/pages/settings/DataSettings/WebDavSettings.tsx index 2fba0d6243..8def14ee52 100644 --- a/src/renderer/src/pages/settings/DataSettings/WebDavSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/WebDavSettings.tsx @@ -201,7 +201,7 @@ const WebDavSettings: FC = () => { {t('settings.data.backup.skip_file_data_title')} - + {t('settings.data.backup.skip_file_data_help')} @@ -209,7 +209,7 @@ const WebDavSettings: FC = () => { {t('settings.data.webdav.disableStream.title')} - + {t('settings.data.webdav.disableStream.help')} diff --git a/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx b/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx index cabbcc7af9..c40ef45631 100644 --- a/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx +++ b/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx @@ -231,7 +231,7 @@ const DisplaySettings: FC = () => { {t('settings.theme.window.style.transparent')} - + )} @@ -355,8 +355,8 @@ const DisplaySettings: FC = () => { {t('settings.advanced.auto_switch_to_topics')} setClickAssistantToShowTopic(checked)} + checked={clickAssistantToShowTopic} + onCheckedChange={(checked) => setClickAssistantToShowTopic(checked)} /> @@ -364,12 +364,12 @@ const DisplaySettings: FC = () => { )} {t('settings.topic.show.time')} - setShowTopicTime(checked)} /> + setShowTopicTime(checked)} /> {t('settings.topic.pin_to_top')} - setPinTopicsToTop(checked)} /> + setPinTopicsToTop(checked)} /> diff --git a/src/renderer/src/pages/settings/GeneralSettings.tsx b/src/renderer/src/pages/settings/GeneralSettings.tsx index 5cbcc63668..b9c9711c4c 100644 --- a/src/renderer/src/pages/settings/GeneralSettings.tsx +++ b/src/renderer/src/pages/settings/GeneralSettings.tsx @@ -268,12 +268,12 @@ const GeneralSettings: FC = () => { /> )} - + {t('settings.hardware_acceleration.title')} - + @@ -289,24 +289,24 @@ const GeneralSettings: FC = () => { /> handleNotificationChange('assistant', v)} + checked={notificationSettings.assistant} + onCheckedChange={(v) => handleNotificationChange('assistant', v)} /> {t('settings.notification.backup')} handleNotificationChange('backup', v)} + checked={notificationSettings.backup} + onCheckedChange={(v) => handleNotificationChange('backup', v)} /> {t('settings.notification.knowledge_embed')} handleNotificationChange('knowledge', v)} + checked={notificationSettings.knowledge} + onCheckedChange={(v) => handleNotificationChange('knowledge', v)} /> @@ -315,12 +315,12 @@ const GeneralSettings: FC = () => { {t('settings.launch.onboot')} - updateLaunchOnBoot(checked)} /> + updateLaunchOnBoot(checked)} /> {t('settings.launch.totray')} - updateLaunchToTray(checked)} /> + updateLaunchToTray(checked)} /> @@ -328,12 +328,12 @@ const GeneralSettings: FC = () => { {t('settings.tray.show')} - updateTray(checked)} /> + updateTray(checked)} /> {t('settings.tray.onclose')} - updateTrayOnClose(checked)} /> + updateTrayOnClose(checked)} /> @@ -342,8 +342,8 @@ const GeneralSettings: FC = () => { {t('settings.privacy.enable_privacy_mode')} { + checked={enableDataCollection} + onCheckedChange={(v) => { setEnableDataCollection(v) window.api.config.set('enableDataCollection', v) }} @@ -358,7 +358,7 @@ const GeneralSettings: FC = () => { {t('settings.developer.enable_developer_mode')} - + diff --git a/src/renderer/src/pages/settings/MCPSettings/McpServerCard.tsx b/src/renderer/src/pages/settings/MCPSettings/McpServerCard.tsx index 15e65ea6e6..57b013fb3c 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpServerCard.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpServerCard.tsx @@ -116,11 +116,10 @@ const McpServerCard: FC = ({ e.stopPropagation()}> diff --git a/src/renderer/src/pages/settings/ModelSettings/DefaultAssistantSettings.tsx b/src/renderer/src/pages/settings/ModelSettings/DefaultAssistantSettings.tsx index 95e4e5986d..badc5803ee 100644 --- a/src/renderer/src/pages/settings/ModelSettings/DefaultAssistantSettings.tsx +++ b/src/renderer/src/pages/settings/ModelSettings/DefaultAssistantSettings.tsx @@ -173,8 +173,8 @@ const AssistantSettings: FC = () => { { + checked={enableTemperature} + onCheckedChange={(enabled) => { setEnableTemperature(enabled) onUpdateAssistantSettings({ enableTemperature: enabled }) }} @@ -215,8 +215,8 @@ const AssistantSettings: FC = () => { { + checked={enableTopP} + onCheckedChange={(enabled) => { setEnableTopP(enabled) onUpdateAssistantSettings({ enableTopP: enabled }) }} @@ -280,8 +280,8 @@ const AssistantSettings: FC = () => { { + checked={enableMaxTokens} + onCheckedChange={async (enabled) => { if (enabled) { const confirmed = await modalConfirm({ title: t('chat.settings.max_tokens.confirm'), diff --git a/src/renderer/src/pages/settings/ModelSettings/QuickModelPopup.tsx b/src/renderer/src/pages/settings/ModelSettings/QuickModelPopup.tsx index 504c9ece3c..4e6bb6be70 100644 --- a/src/renderer/src/pages/settings/ModelSettings/QuickModelPopup.tsx +++ b/src/renderer/src/pages/settings/ModelSettings/QuickModelPopup.tsx @@ -59,7 +59,7 @@ const PopupContainer: React.FC = ({ resolve }) => {
{t('settings.models.topic_naming.auto')}
- +
diff --git a/src/renderer/src/pages/settings/NotesSettings.tsx b/src/renderer/src/pages/settings/NotesSettings.tsx index 6f00f81c5f..70d8f92a69 100644 --- a/src/renderer/src/pages/settings/NotesSettings.tsx +++ b/src/renderer/src/pages/settings/NotesSettings.tsx @@ -164,8 +164,8 @@ const NotesSettings: FC = () => { {t('notes.settings.display.compress_content')} updateSettings({ isFullWidth: !checked })} + checked={!settings.isFullWidth} + onCheckedChange={(checked) => updateSettings({ isFullWidth: !checked })} /> {t('notes.settings.display.compress_content_description')} @@ -188,8 +188,8 @@ const NotesSettings: FC = () => { {t('notes.settings.display.show_table_of_contents')} updateSettings({ showTableOfContents: checked })} + checked={settings.showTableOfContents} + onCheckedChange={(checked) => updateSettings({ showTableOfContents: checked })} /> {t('notes.settings.display.show_table_of_contents_description')} diff --git a/src/renderer/src/pages/settings/ProviderSettings/ApiOptionsSettings/ApiOptionsSettings.tsx b/src/renderer/src/pages/settings/ProviderSettings/ApiOptionsSettings/ApiOptionsSettings.tsx index fab07d55d0..4ea5cdc050 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ApiOptionsSettings/ApiOptionsSettings.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ApiOptionsSettings/ApiOptionsSettings.tsx @@ -124,7 +124,7 @@ const ApiOptionsSettings = ({ providerId }: Props) => { - + ))} diff --git a/src/renderer/src/pages/settings/ProviderSettings/EditModelPopup/ModelEditContent.tsx b/src/renderer/src/pages/settings/ProviderSettings/EditModelPopup/ModelEditContent.tsx index d34f9653b8..5439042c86 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/EditModelPopup/ModelEditContent.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/EditModelPopup/ModelEditContent.tsx @@ -343,10 +343,9 @@ const ModelEditContent: FC = ({ provider, mo label={t('settings.models.add.supported_text_delta.label')} tooltip={t('settings.models.add.supported_text_delta.tooltip')}> { + onCheckedChange={(checked) => { setSupportedTextDelta(checked) // 直接传递新值给autoSave autoSave({ supported_text_delta: checked }) diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx index c6c856a7e4..d06b72e095 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx @@ -410,9 +410,9 @@ const ProviderSetting: FC = ({ providerId }) => { )} { + onCheckedChange={(enabled) => { updateProvider({ apiHost, enabled }) if (enabled) { moveProviderToTop(provider.id) diff --git a/src/renderer/src/pages/settings/QuickAssistantSettings.tsx b/src/renderer/src/pages/settings/QuickAssistantSettings.tsx index 34b495fbc6..6828e01eb9 100644 --- a/src/renderer/src/pages/settings/QuickAssistantSettings.tsx +++ b/src/renderer/src/pages/settings/QuickAssistantSettings.tsx @@ -82,14 +82,14 @@ const QuickAssistantSettings: FC = () => { iconProps={{ className: 'cursor-pointer' }} /> - + {enableQuickAssistant && ( <> {t('settings.quickAssistant.click_tray_to_show')} - + )} @@ -98,7 +98,7 @@ const QuickAssistantSettings: FC = () => { {t('settings.quickAssistant.read_clipboard_at_startup')} - + )} diff --git a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx b/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx index 81b55a20c5..55bdb2ed0e 100644 --- a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx +++ b/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx @@ -101,8 +101,8 @@ const SelectionAssistantSettings: FC = () => { {!isSupportedOS && {t('selection.settings.enable.description')}} @@ -162,7 +162,7 @@ const SelectionAssistantSettings: FC = () => { {t('selection.settings.toolbar.compact_mode.title')} {t('selection.settings.toolbar.compact_mode.description')} - + @@ -174,7 +174,7 @@ const SelectionAssistantSettings: FC = () => { {t('selection.settings.window.follow_toolbar.title')} {t('selection.settings.window.follow_toolbar.description')} - + @@ -182,7 +182,7 @@ const SelectionAssistantSettings: FC = () => { {t('selection.settings.window.remember_size.title')} {t('selection.settings.window.remember_size.description')} - + @@ -190,7 +190,7 @@ const SelectionAssistantSettings: FC = () => { {t('selection.settings.window.auto_close.title')} {t('selection.settings.window.auto_close.description')} - + @@ -198,7 +198,7 @@ const SelectionAssistantSettings: FC = () => { {t('selection.settings.window.auto_pin.title')} {t('selection.settings.window.auto_pin.description')} - + diff --git a/src/renderer/src/pages/settings/ShortcutSettings.tsx b/src/renderer/src/pages/settings/ShortcutSettings.tsx index e42ee98032..f9c661808e 100644 --- a/src/renderer/src/pages/settings/ShortcutSettings.tsx +++ b/src/renderer/src/pages/settings/ShortcutSettings.tsx @@ -392,7 +392,7 @@ const ShortcutSettings: FC = () => { align: 'right', width: '50px', render: (record: Shortcut) => ( - dispatch(toggleShortcut(record.key))} /> + dispatch(toggleShortcut(record.key))} /> ) } ] diff --git a/src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx b/src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx index 5b3827929c..aa4a4f6590 100644 --- a/src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx +++ b/src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx @@ -23,7 +23,7 @@ const BasicSettings: FC = () => { {t('settings.tool.websearch.search_with_time')} - dispatch(setSearchWithTime(checked))} /> + dispatch(setSearchWithTime(checked))} /> diff --git a/src/renderer/src/pages/translate/TranslateSettings.tsx b/src/renderer/src/pages/translate/TranslateSettings.tsx index 1fcb899a9d..e82cc7c903 100644 --- a/src/renderer/src/pages/translate/TranslateSettings.tsx +++ b/src/renderer/src/pages/translate/TranslateSettings.tsx @@ -67,8 +67,8 @@ const TranslateSettings: FC<{
{t('translate.settings.preview')}
{ + checked={enableMarkdown} + onCheckedChange={(checked) => { setEnableMarkdown(checked) db.settings.put({ id: 'translate:markdown:enabled', value: checked }) }} @@ -80,9 +80,9 @@ const TranslateSettings: FC<{
{t('translate.settings.autoCopy')}
{ + onCheckedChange={(isSelected) => { updateSettings({ autoCopy: isSelected }) }} /> @@ -93,9 +93,9 @@ const TranslateSettings: FC<{
{t('translate.settings.scroll_sync')}
{ + onCheckedChange={(isSelected) => { setIsScrollSyncEnabled(isSelected) db.settings.put({ id: 'translate:scroll:sync', value: isSelected }) }} @@ -145,9 +145,9 @@ const TranslateSettings: FC<{
{ + onCheckedChange={(isSelected) => { setIsBidirectional(isSelected) // 双向翻译设置不需要持久化,它只是界面状态 }} diff --git a/yarn.lock b/yarn.lock index e563de008d..988f760cfd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -231,7 +231,7 @@ __metadata: languageName: node linkType: hard -"@ai-sdk/openai-compatible@npm:1.0.27, @ai-sdk/openai-compatible@npm:^1.0.19": +"@ai-sdk/openai-compatible@npm:1.0.27": version: 1.0.27 resolution: "@ai-sdk/openai-compatible@npm:1.0.27" dependencies: @@ -243,6 +243,18 @@ __metadata: languageName: node linkType: hard +"@ai-sdk/openai-compatible@npm:^1.0.19": + version: 1.0.19 + resolution: "@ai-sdk/openai-compatible@npm:1.0.19" + dependencies: + "@ai-sdk/provider": "npm:2.0.0" + "@ai-sdk/provider-utils": "npm:3.0.10" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/5b7b21fb515e829c3d8a499a5760ffc035d9b8220695996110e361bd79e9928859da4ecf1ea072735bcbe4977c6dd0661f543871921692e86f8b5bfef14fe0e5 + languageName: node + linkType: hard + "@ai-sdk/openai-compatible@patch:@ai-sdk/openai-compatible@npm%3A1.0.27#~/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.27-06f74278cf.patch": version: 1.0.27 resolution: "@ai-sdk/openai-compatible@patch:@ai-sdk/openai-compatible@npm%3A1.0.27#~/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.27-06f74278cf.patch::version=1.0.27&hash=c44b76" @@ -291,6 +303,19 @@ __metadata: languageName: node linkType: hard +"@ai-sdk/provider-utils@npm:3.0.10": + version: 3.0.10 + resolution: "@ai-sdk/provider-utils@npm:3.0.10" + dependencies: + "@ai-sdk/provider": "npm:2.0.0" + "@standard-schema/spec": "npm:^1.0.0" + eventsource-parser: "npm:^3.0.5" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/d2c16abdb84ba4ef48c9f56190b5ffde224b9e6ae5147c5c713d2623627732d34b96aa9aef2a2ea4b0c49e1b863cc963c7d7ff964a1dc95f0f036097aaaaaa98 + languageName: node + linkType: hard + "@ai-sdk/provider-utils@npm:3.0.17, @ai-sdk/provider-utils@npm:^3.0.10, @ai-sdk/provider-utils@npm:^3.0.17": version: 3.0.17 resolution: "@ai-sdk/provider-utils@npm:3.0.17" @@ -7730,6 +7755,31 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-switch@npm:^1.2.6": + version: 1.2.6 + resolution: "@radix-ui/react-switch@npm:1.2.6" + dependencies: + "@radix-ui/primitive": "npm:1.1.3" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-controllable-state": "npm:1.2.2" + "@radix-ui/react-use-previous": "npm:1.1.1" + "@radix-ui/react-use-size": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/888303cbeb0e69ebba5676b225f9ea0f00f61453c6b8a6b66384b5c5c4c7fb0ccc53493c1eb14ec6d436e5b867b302aadd6af51a1f2e6c04581c583fd9be65be + languageName: node + linkType: hard + "@radix-ui/react-tabs@npm:^1.1.13": version: 1.1.13 resolution: "@radix-ui/react-tabs@npm:1.1.13" @@ -13822,6 +13872,7 @@ __metadata: "@paymoapp/electron-shutdown-handler": "npm:^1.1.2" "@playwright/test": "npm:^1.55.1" "@radix-ui/react-context-menu": "npm:^2.2.16" + "@radix-ui/react-switch": "npm:^1.2.6" "@reduxjs/toolkit": "npm:^2.2.5" "@shikijs/markdown-it": "npm:^3.12.0" "@strongtz/win32-arm64-msvc": "npm:^0.4.7" @@ -18519,7 +18570,7 @@ __metadata: languageName: node linkType: hard -"eventsource-parser@npm:^3.0.0": +"eventsource-parser@npm:^3.0.0, eventsource-parser@npm:^3.0.5": version: 3.0.5 resolution: "eventsource-parser@npm:3.0.5" checksum: 10c0/5cb75e3f84ff1cfa1cee6199d4fd430c4544855ab03e953ddbe5927e7b31bc2af3933ab8aba6440ba160ed2c48972b6c317f27b8a1d0764c7b12e34e249de631