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 ef610da5f..2ff7fd9f4 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 794e0ab30..4e1ac0061 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 51d972160..7ce2ac5d3 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 000000000..9587870e2 --- /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 000000000..958701496 --- /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 8d4a2fab3..1a2ce5117 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 68a14cca2..b5d3709ec 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 d71df5cd9..19c232498 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 cbc688155..294bed47f 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 09cdf1dc2..7f83183d2 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 0fc025c22..16ff35363 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 ce1dbbe2d..cf8735c3b 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 f55b56775..571914ac4 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 40dc2249a..22126c368 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 d9bb273d8..64f66e9cd 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 60cc2b5b8..b323b6dfc 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 120b10103..1a981251f 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 0e9bba3ef..5b98bda4f 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 974f76023..878560d47 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 b5b72af37..d34affe07 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 5278540af..f70c3270a 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 738466031..c9c8497c7 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 25e0dd1b1..eb9d116db 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 4a6ab7c2d..ec7f81345 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 2da603ad6..2f1fe2507 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 2fba0d624..8def14ee5 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 cabbcc7af..c40ef4563 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 5cbcc6366..b9c9711c4 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 15e65ea6e..57b013fb3 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 95e4e5986..badc5803e 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 504c9ece3..4e6bb6be7 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 6f00f81c5..70d8f92a6 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 fab07d55d..4ea5cdc05 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 d34f9653b..5439042c8 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 c6c856a7e..d06b72e09 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 34b495fbc..6828e01eb 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 81b55a20c..55bdb2ed0 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 e42ee9803..f9c661808 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 5b3827929..aa4a4f659 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 1fcb899a9..e82cc7c90 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 e563de008..988f760cf 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